001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://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: https://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: https://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.ui.dialogs;
029
030import org.opencms.db.CmsResourceState;
031import org.opencms.file.CmsObject;
032import org.opencms.file.CmsProperty;
033import org.opencms.file.CmsPropertyDefinition;
034import org.opencms.file.CmsResource;
035import org.opencms.file.CmsResourceFilter;
036import org.opencms.file.types.CmsResourceTypeImage;
037import org.opencms.lock.CmsLockActionRecord;
038import org.opencms.lock.CmsLockUtil;
039import org.opencms.main.CmsException;
040import org.opencms.main.CmsLog;
041import org.opencms.main.OpenCms;
042import org.opencms.relations.CmsRelation;
043import org.opencms.relations.CmsRelationFilter;
044import org.opencms.ui.A_CmsUI;
045import org.opencms.ui.CmsVaadinUtils;
046import org.opencms.ui.FontOpenCms;
047import org.opencms.ui.I_CmsDialogContext;
048import org.opencms.ui.apps.CmsAppWorkplaceUi;
049import org.opencms.ui.components.CmsBasicDialog;
050import org.opencms.ui.components.CmsConfirmationDialog;
051import org.opencms.ui.components.CmsGwtContextMenuButton;
052import org.opencms.ui.components.CmsOkCancelActionHandler;
053import org.opencms.ui.components.CmsResourceInfo;
054import org.opencms.ui.components.OpenCmsTheme;
055import org.opencms.ui.shared.rpc.I_CmsGwtContextMenuServerRpc;
056import org.opencms.ui.util.CmsGalleryFilePreview;
057import org.opencms.util.CmsDateUtil;
058import org.opencms.util.CmsStringUtil;
059import org.opencms.util.CmsUUID;
060import org.opencms.workplace.explorer.CmsResourceUtil;
061
062import java.text.DateFormat;
063import java.util.ArrayList;
064import java.util.Arrays;
065import java.util.Collections;
066import java.util.Comparator;
067import java.util.Date;
068import java.util.HashSet;
069import java.util.List;
070import java.util.Set;
071import java.util.concurrent.TimeUnit;
072import java.util.stream.Collectors;
073
074import org.apache.commons.logging.Log;
075
076import com.vaadin.data.Binder;
077import com.vaadin.data.ValidationException;
078import com.vaadin.data.provider.ListDataProvider;
079import com.vaadin.data.provider.Query;
080import com.vaadin.server.Page;
081import com.vaadin.server.SerializableComparator;
082import com.vaadin.server.SerializablePredicate;
083import com.vaadin.server.VaadinService;
084import com.vaadin.server.WrappedSession;
085import com.vaadin.shared.Position;
086import com.vaadin.shared.ui.ContentMode;
087import com.vaadin.shared.ui.MarginInfo;
088import com.vaadin.ui.AbsoluteLayout;
089import com.vaadin.ui.Alignment;
090import com.vaadin.ui.Button;
091import com.vaadin.ui.CheckBox;
092import com.vaadin.ui.Component;
093import com.vaadin.ui.CssLayout;
094import com.vaadin.ui.FormLayout;
095import com.vaadin.ui.GridLayout;
096import com.vaadin.ui.HorizontalLayout;
097import com.vaadin.ui.ItemCaptionGenerator;
098import com.vaadin.ui.Label;
099import com.vaadin.ui.NativeSelect;
100import com.vaadin.ui.Notification;
101import com.vaadin.ui.Panel;
102import com.vaadin.ui.TextField;
103import com.vaadin.ui.VerticalLayout;
104import com.vaadin.ui.Window;
105import com.vaadin.ui.themes.ValoTheme;
106
107/**
108 * Class representing a dialog for optimizing galleries.<p>
109 */
110public class CmsGalleryOptimizeDialog extends CmsBasicDialog {
111
112    /**
113     * The context used for child dialogs.<p>
114     */
115    public class ContextMenu implements I_CmsGwtContextMenuServerRpc {
116
117        /** Default serial version uid. */
118        private static final long serialVersionUID = 1L;
119
120        /**
121         * The data item handled by this dialog context.
122         */
123        private final DataItem m_dataItem;
124
125        /**
126         * Creates a new instance.
127        
128         * @param dataItem the data item
129         */
130        public ContextMenu(DataItem dataItem) {
131
132            m_dataItem = dataItem;
133        }
134
135        /**
136         * @see org.opencms.ui.shared.rpc.I_CmsGwtContextMenuServerRpc#refresh(java.lang.String)
137         */
138        public void refresh(String uuid) {
139
140            if (uuid != null) {
141                try {
142                    TimeUnit.SECONDS.sleep(1);
143                    CmsResource resource = getCms().readResource(new CmsUUID(uuid), CmsResourceFilter.ONLY_VISIBLE);
144                    boolean deleted = resource.getState() == CmsResourceState.STATE_DELETED;
145                    if (deleted) {
146                        CmsGalleryOptimizeDialog.this.handleDataListDelete(Arrays.asList(m_dataItem));
147                    } else {
148                        CmsGalleryOptimizeDialog.this.handleDataListUpdate(Arrays.asList(m_dataItem));
149                    }
150                    String message = CmsVaadinUtils.getMessageText(
151                        Messages.GUI_GALLERY_OPTIMIZE_LABEL_SUCCESSFULLY_SAVED_0);
152                    Notification notification = new Notification(message, "", Notification.Type.HUMANIZED_MESSAGE);
153                    notification.setPosition(Position.TOP_CENTER);
154                    notification.show(Page.getCurrent());
155                    CmsAppWorkplaceUi.get().enableGlobalShortcuts();
156                } catch (CmsException | InterruptedException e) {
157                    LOG.error(e.getLocalizedMessage(), e);
158                    Notification notification = new Notification(
159                        "",
160                        e.getLocalizedMessage(),
161                        Notification.Type.ERROR_MESSAGE);
162                    notification.setHtmlContentAllowed(true);
163                    notification.setPosition(Position.TOP_CENTER);
164                    notification.show(Page.getCurrent());
165                }
166            }
167        }
168    }
169
170    /**
171     * Class representing an editable gallery item.<p>
172     */
173    private class DataItem {
174
175        /** The data binder of this editable gallery item. */
176        private Binder<DataItem> m_binder = new Binder<DataItem>();
177
178        /** The file composite of this editable gallery item. */
179        private FileComposite m_compositeFile;
180
181        /** The file delete composite of this editable gallery item. */
182        private FileDeleteComposite m_compositeFileDelete;
183
184        /** The form composite of this editable gallery item. */
185        private FormComposite m_compositeForm;
186
187        /** The copyright information of this editable gallery item. */
188        private String m_copyright;
189
190        /** Date when this editable gallery item was last modified. */
191        private Long m_dateLastModified;
192
193        /** Whether this editable gallery item shall be deleted. */
194        private Boolean m_deleteFlag = Boolean.valueOf(false);
195
196        /** The description of this editable gallery item. */
197        private String m_description;
198
199        /** Whether this editable gallery item is used. */
200        private Boolean m_isUsed;
201
202        /** The file name of this editable gallery item. */
203        private String m_name;
204
205        /** The full path of this editable gallery item. */
206        private String m_path;
207
208        /** The CMS resource of this editable gallery item. */
209        private CmsResource m_resource;
210
211        /** The CMS resource utility of this editable gallery item. */
212        private CmsResourceUtil m_resourceUtil;
213
214        /** The title of this editable gallery item. */
215        private String m_title;
216
217        /**
218         * Creates a new editable gallery item for a given CMS resource.<p>
219         *
220         * @param resource the CMS resource
221         */
222        public DataItem(CmsResource resource) {
223
224            m_resource = resource;
225            initData();
226            initComponent();
227        }
228
229        /**
230         * Returns the binder of this editable gallery item.<p>
231         *
232         * @return the binder
233         */
234        public Binder<DataItem> getBinder() {
235
236            return m_binder;
237        }
238
239        /**
240         * Returns the file composite of this editable gallery item.<p>
241         *
242         * @return the file composite
243         */
244        public FileComposite getCompositeFile() {
245
246            return m_compositeFile;
247        }
248
249        /**
250         * Returns the file delete composite of this editable gallery item.<p>
251         *
252         * @return the file delete composite
253         */
254        public FileDeleteComposite getCompositeFileDelete() {
255
256            return m_compositeFileDelete;
257        }
258
259        /**
260         * Returns the form composite of this editable gallery item.<p>
261         *
262         * @return the form composite
263         */
264        public FormComposite getCompositeForm() {
265
266            return m_compositeForm;
267        }
268
269        /**
270         * Returns the copyright information of this editable gallery item.<p>
271         *
272         * @return the copyright information
273         */
274        public String getCopyright() {
275
276            return m_copyright;
277        }
278
279        /**
280         * Returns the date when this editable gallery item was last modified.<p>
281         *
282         * @return the date
283         */
284        public Long getDateLastModified() {
285
286            return m_dateLastModified;
287        }
288
289        /**
290         * Returns whether this editable gallery item shall be deleted.<p>
291         *
292         * @return whether delete or not
293         */
294        public Boolean getDeleteFlag() {
295
296            return m_deleteFlag;
297        }
298
299        /**
300         * Returns the description of this editable gallery item.<p>
301         *
302         * @return the description
303         */
304        public String getDescription() {
305
306            return m_description;
307        }
308
309        /**
310         * Returns the filter text.<p>
311         *
312         * @return the filter text
313         */
314        public String getFilterText() {
315
316            return (m_name + " " + m_title + " " + m_copyright + " " + m_description).toLowerCase();
317        }
318
319        /**
320         * Returns whether this editable gallery item is used.<p>
321         *
322         * @return whether used or not
323         */
324        public Boolean getIsUsed() {
325
326            return m_isUsed;
327        }
328
329        /**
330         * Returns the name of this editable gallery item.<p>
331         *
332         * @return the name
333         */
334        public String getName() {
335
336            return m_name;
337        }
338
339        /**
340         * Returns whether this data item has no copyright information.<p>
341         *
342         * @return whether this data item has no copyright information
343         */
344        public Boolean getNoCopyright() {
345
346            return Boolean.valueOf(CmsStringUtil.isNotEmptyOrWhitespaceOnly(m_copyright));
347        }
348
349        /**
350         * Returns whether this data item has no description.<p>
351         *
352         * @return whether this data item has no description
353         */
354        public Boolean getNoDescription() {
355
356            return Boolean.valueOf(CmsStringUtil.isNotEmptyOrWhitespaceOnly(m_description));
357        }
358
359        /**
360         * Returns the full path of this editable gallery item.<p>
361         *
362         * @return the full path
363         */
364        public String getPath() {
365
366            return m_path;
367        }
368
369        /**
370         * Converts the form data of this editable gallery item into a list of CMS properties.<p>
371         *
372         * @return the CMS property list
373         */
374        public List<CmsProperty> getPropertyList() {
375
376            List<CmsProperty> propertyList = new ArrayList<CmsProperty>();
377            propertyList.add(new CmsProperty(CmsPropertyDefinition.PROPERTY_TITLE, m_title, null));
378            propertyList.add(new CmsProperty(CmsPropertyDefinition.PROPERTY_COPYRIGHT, m_copyright, null));
379            propertyList.add(new CmsProperty(CmsPropertyDefinition.PROPERTY_DESCRIPTION, m_description, null));
380            return propertyList;
381        }
382
383        /**
384         * Returns the CMS resource this editable gallery item was created from.<p>
385         *
386         * @return the CMS resource
387         */
388        public CmsResource getResource() {
389
390            return m_resource;
391        }
392
393        /**
394         * Returns the CMS resource utility class for this editable gallery item.<p>
395         *
396         * @return the CMS resource utility class
397         */
398        public CmsResourceUtil getResourceUtil() {
399
400            return m_resourceUtil;
401        }
402
403        /**
404         * Returns the title of this editable gallery item.<p>
405         *
406         * @return the title
407         */
408        public String getTitle() {
409
410            return m_title;
411        }
412
413        /**
414         * Returns whether this editable gallery item has value changes compared
415         * to the property values actually persisted.<p>
416         *
417         * @return whether changes or not
418         */
419        public boolean hasChanges() {
420
421            boolean hasChanges = false;
422            try {
423                if (!hasChanges) {
424                    hasChanges = !m_title.equals(readPropertyTitle());
425                }
426                if (!hasChanges) {
427                    hasChanges = !m_copyright.equals(readPropertyCopyright());
428                }
429                if (!hasChanges) {
430                    hasChanges = !m_description.equals(readPropertyDescription());
431                }
432            } catch (CmsException e) {
433                hasChanges = true;
434                LOG.warn(e.getLocalizedMessage(), e);
435            }
436            return hasChanges;
437        }
438
439        /**
440         * Returns whether this editable gallery item is renamed compared to
441         * the resource actually persisted.<p>
442         *
443         * @return whether renamed or not
444         */
445        public boolean isRenamed() {
446
447            boolean isRenamed = false;
448            try {
449                isRenamed = !m_name.equals(readName());
450            } catch (CmsException e) {
451                LOG.warn(e.getLocalizedMessage(), e);
452            }
453            return isRenamed;
454        }
455
456        /**
457         * Returns whether this editable gallery item is an image.<p>
458         *
459         * @return whether an image or not
460         */
461        public boolean isTypeImage() {
462
463            return OpenCms.getResourceManager().getResourceType(m_resource) instanceof CmsResourceTypeImage;
464        }
465
466        /**
467         * Sets the copyright information of this editable gallery item.<p>
468         *
469         * @param copyright the copyright information
470         */
471        public void setCopyright(String copyright) {
472
473            m_copyright = copyright;
474        }
475
476        /**
477         * Sets the flag that states whether this editable gallery item shall be deleted.<p>
478         *
479         * @param deleteFlag the flag
480         */
481        public void setDeleteFlag(Boolean deleteFlag) {
482
483            m_deleteFlag = deleteFlag;
484        }
485
486        /**
487         * Sets the description of this editable gallery item.<p>
488         *
489         * @param description the description
490         */
491        public void setDescription(String description) {
492
493            m_description = description;
494        }
495
496        /**
497         * Sets the name of this editable gallery item.<p>
498         *
499         * @param name the name
500         */
501        public void setName(String name) {
502
503            m_name = name;
504        }
505
506        /**
507         * Sets the CMS resource of this editable gallery item and re-initializes all data and components.<p>
508         *
509         * @param resource the CMS resource
510         */
511        public void setResource(CmsResource resource) {
512
513            m_resource = resource;
514            initData();
515            initComponent();
516        }
517
518        /**
519         * Sets the title of this editable gallery item.<p>
520         *
521         * @param title the title
522         */
523        public void setTitle(String title) {
524
525            m_title = title;
526        }
527
528        /**
529         * Initializes all UI components of this editable gallery item and initializes all data fields.<p>
530         */
531        private void initComponent() {
532
533            m_compositeFile = new FileComposite(this);
534            m_compositeFileDelete = new FileDeleteComposite(this);
535            m_compositeForm = new FormComposite(this);
536            m_binder.readBean(this);
537        }
538
539        /**
540         * Initializes all data of this editable gallery item.<p>
541         */
542        private void initData() {
543
544            m_resourceUtil = new CmsResourceUtil(A_CmsUI.getCmsObject(), m_resource);
545            try {
546                List<CmsRelation> relations = getCms().getRelationsForResource(m_resource, CmsRelationFilter.SOURCES);
547                m_name = m_resource.getName();
548                m_path = m_resource.getRootPath();
549                m_title = readPropertyTitle();
550                m_copyright = readPropertyCopyright();
551                m_description = readPropertyDescription();
552                m_dateLastModified = Long.valueOf(m_resource.getDateLastModified());
553                m_isUsed = Boolean.valueOf(!((relations == null) || relations.isEmpty()));
554            } catch (CmsException e) {
555                LOG.error(e.getLocalizedMessage(), e);
556            }
557        }
558
559        /**
560         * Reads the persisted resource name.<p>
561         *
562         * @return the resource name
563         * @throws CmsException thrown if reading the resource fails
564         */
565        private String readName() throws CmsException {
566
567            CmsResourceFilter resourceFilter = CmsResourceFilter.IGNORE_EXPIRATION.addRequireFile();
568            return getCms().readResource(m_resource.getStructureId(), resourceFilter).getName();
569        }
570
571        /**
572         * Reads the persisted copyright property value.<p>
573         *
574         * @return the copyright property value
575         * @throws CmsException thrown if the property read fails
576         */
577        private String readPropertyCopyright() throws CmsException {
578
579            String value = getCms().readPropertyObject(
580                m_resource,
581                CmsPropertyDefinition.PROPERTY_COPYRIGHT,
582                false).getValue();
583            return value == null ? "" : value;
584        }
585
586        /**
587         * Reads the persisted description property value.<p>
588         *
589         * @return the description property value
590         * @throws CmsException thrown if the property read fails
591         */
592        private String readPropertyDescription() throws CmsException {
593
594            String value = getCms().readPropertyObject(
595                m_resource,
596                CmsPropertyDefinition.PROPERTY_DESCRIPTION,
597                false).getValue();
598            return value == null ? "" : value;
599        }
600
601        /**
602         * Reads the persisted title property value.<p>
603         *
604         * @return the title property value
605         * @throws CmsException thrown if the property read fails
606         */
607        private String readPropertyTitle() throws CmsException {
608
609            String value = getCms().readPropertyObject(
610                m_resource,
611                CmsPropertyDefinition.PROPERTY_TITLE,
612                false).getValue();
613            return value == null ? "" : value;
614        }
615    }
616
617    /**
618     * Class representing the data list header view with components for
619     * sorting, paging, and filtering the gallery item list.<p>
620     */
621    private class DataListHeaderComposite extends AbsoluteLayout {
622
623        /** The default serial version UID. */
624        private static final long serialVersionUID = 1L;
625
626        /** The page info label. */
627        private Label m_labelPageInfo;
628
629        /** The select box for page selection. */
630        private NativeSelect<Integer> m_selectPage;
631
632        /** The select box for sort order selection. */
633        private NativeSelect<String> m_selectSortOrder;
634
635        /** The text field for filtering. */
636        private TextField m_textFieldFilter;
637
638        /**
639         * Creates a new data list header composite.<p>
640         */
641        public DataListHeaderComposite() {
642
643            setHeight("34px");
644            setWidthFull();
645            m_selectSortOrder = createSelectSortOrder();
646            m_textFieldFilter = createTextFieldFilter();
647
648            addComponent(m_selectSortOrder, "left: 0px; top: 2px;");
649            addComponent(m_textFieldFilter, "right: 0px; top: 2px;");
650            addStyleName("o-optimize-gallery-header");
651            refresh();
652        }
653
654        /**
655         * Refreshes this component.<p>
656         */
657        public void refresh() {
658
659            NativeSelect<Integer> selectPage = createSelectPage();
660            Label pageInfo = createLabelPageInfo();
661            if (m_selectPage == null) {
662                m_selectPage = selectPage;
663                addComponent(m_selectPage, "left: 436px; top: 2px;");
664            } else {
665                replaceComponent(m_selectPage, selectPage);
666                m_selectPage = selectPage;
667            }
668            if (m_labelPageInfo == null) {
669                m_labelPageInfo = pageInfo;
670                addComponent(m_labelPageInfo, "right: 228px; top: 6px;");
671            } else {
672                replaceComponent(m_labelPageInfo, pageInfo);
673                m_labelPageInfo = pageInfo;
674            }
675        }
676
677        /**
678         * Programmatically selects a page according to a given index.<p>
679         *
680         * @param index the page index
681         */
682        public void selectPage(int index) {
683
684            m_selectPage.setValue(null);
685            handlePageChange(index, false);
686        }
687
688        /**
689         * Creates a page info label.<p>
690         *
691         * @return the page info label
692         */
693        private Label createLabelPageInfo() {
694
695            String text = "";
696            if (m_pageHandler.hasPages()) {
697                text = CmsVaadinUtils.getMessageText(
698                    Messages.GUI_GALLERY_OPTIMIZE_LABEL_PAGE_INFO_3,
699                    String.valueOf(m_pageHandler.getNumFirstItem()),
700                    String.valueOf(m_pageHandler.getNumLastItem()),
701                    String.valueOf(m_pageHandler.getSizeItem()));
702            } else if (m_pageHandler.getSizeItem() == 1) {
703                text = CmsVaadinUtils.getMessageText(
704                    Messages.GUI_GALLERY_OPTIMIZE_LABEL_PAGE_INFO_ONE_0,
705                    String.valueOf(m_pageHandler.getSizeItem()));
706            } else {
707                text = CmsVaadinUtils.getMessageText(
708                    Messages.GUI_GALLERY_OPTIMIZE_LABEL_PAGE_INFO_1,
709                    String.valueOf(m_pageHandler.getSizeItem()));
710            }
711            Label label = new Label(text);
712            label.setWidthUndefined();
713            return label;
714        }
715
716        /**
717         * Creates a select box for page select.<p>
718         *
719         * @return the page select box
720         */
721        private NativeSelect<Integer> createSelectPage() {
722
723            NativeSelect<Integer> selectPage = new NativeSelect<Integer>();
724            selectPage.setWidthUndefined();
725            int numPages = m_pageHandler.getNumPages();
726            selectPage.setItemCaptionGenerator(new ItemCaptionGenerator<Integer>() {
727
728                private static final long serialVersionUID = 1L;
729
730                public String apply(Integer item) {
731
732                    return CmsVaadinUtils.getMessageText(
733                        Messages.GUI_GALLERY_OPTIMIZE_SELECTED_PAGE_2,
734                        String.valueOf(item.intValue() + 1),
735                        String.valueOf(numPages));
736                }
737
738            });
739            Integer firstItem = Integer.valueOf(0);
740            List<Integer> items = new ArrayList<Integer>();
741            for (int i = 0; i < numPages; i++) {
742                if (i > 0) {
743                    items.add(Integer.valueOf(i));
744                }
745            }
746            selectPage.setEmptySelectionCaption(selectPage.getItemCaptionGenerator().apply(firstItem));
747            selectPage.setItems(items);
748            selectPage.addValueChangeListener(event -> {
749                if (event.isUserOriginated()) {
750                    int index = event.getValue() != null ? event.getValue().intValue() : 0;
751                    handlePageChange(index, true);
752                }
753            });
754            selectPage.setVisible(m_pageHandler.hasPages());
755            return selectPage;
756        }
757
758        /**
759         * Creates a select box for sort order select.<p>
760         *
761         * @return the sort order select box
762         */
763        private NativeSelect<String> createSelectSortOrder() {
764
765            NativeSelect<String> selectSortOrder = new NativeSelect<String>();
766            selectSortOrder.setWidthUndefined();
767            selectSortOrder.setEmptySelectionCaption(m_messageSortTitleAscending);
768            selectSortOrder.setItems(
769                m_messageSortTitleDescending,
770                m_messageSortDateLastModifiedAscending,
771                m_messageSortDateLastModifiedDescending,
772                m_messageSortPathAscending,
773                m_messageSortPathDescending,
774                m_messageSortUnusedFirst,
775                m_messageSortNoCopyrightFirst,
776                m_messageSortNoDescriptionFirst);
777            selectSortOrder.addValueChangeListener(event -> {
778                if (event.isUserOriginated()) {
779                    selectPage(0);
780                    m_provider.refreshAll();
781                    displayDataListViewSorted(event.getValue());
782                }
783            });
784            selectSortOrder.setValue(getSessionSortOrder());
785            return selectSortOrder;
786        }
787
788        /**
789         * Creates a text field for filtering.<p>
790         *
791         * @return the filter text field
792         */
793        private TextField createTextFieldFilter() {
794
795            TextField textField = new TextField();
796            textField.setPlaceholder(CmsVaadinUtils.getMessageText(org.opencms.ui.apps.Messages.GUI_EXPLORER_FILTER_0));
797            textField.setWidth("200px");
798            textField.addStyleName(ValoTheme.TEXTFIELD_INLINE_ICON);
799            textField.setIcon(FontOpenCms.FILTER);
800            textField.addValueChangeListener(event -> {
801                handleFilterChange(event.getValue());
802            });
803            return textField;
804        }
805
806        /**
807         * Filter change event handler. Updates the page info label and the gallery list.<p>
808         *
809         * @param query the filter query string
810         */
811        private void handleFilterChange(String query) {
812
813            String clean = query.trim();
814            m_filterHandler.setQuery(clean);
815            m_pageHandler.setCurrentPage(0);
816            refresh();
817            displayDataListView(true);
818        }
819
820        /**
821         * Page change event handler. Updates the page info label.<p>
822         *
823         * @param index the index of the page to select
824         * @param display whether to re-render the data item list
825         */
826        private void handlePageChange(int index, boolean display) {
827
828            m_pageHandler.setCurrentPage(index);
829            Label label = createLabelPageInfo();
830            replaceComponent(m_labelPageInfo, label);
831            m_labelPageInfo = label;
832            if (display) {
833                displayDataListView(true);
834            }
835        }
836    }
837
838    /**
839     * Class representing a file composite offering a file preview.<p>
840     */
841    private class FileComposite extends HorizontalLayout {
842
843        /** The panel height. */
844        private static final String PANEL_HEIGHT = "172px";
845
846        /** The panel width. */
847        private static final String PANEL_WIDTH = "202px";
848
849        /** The default serial version UID. */
850        private static final long serialVersionUID = 1L;
851
852        /** The main panel of this file composite. */
853        private AbsoluteLayout m_panel;
854
855        /**
856         * Creates a new file composite for a given data item.<p>
857         *
858         * @param dataItem the data item
859         */
860        public FileComposite(DataItem dataItem) {
861
862            setSizeUndefined();
863            setMargin(true);
864            m_panel = new AbsoluteLayout();
865            m_panel.setWidth(PANEL_WIDTH);
866            m_panel.setHeight(PANEL_HEIGHT);
867            m_panel.addStyleName("v-panel");
868            Component link = CmsGalleryFilePreview.createClickableFile(
869                dataItem.getResource(),
870                dataItem.getResourceUtil());
871            m_panel.addComponent(link, "left: 0px; top: 0px;");
872            m_panel.addStyleName("o-optimize-gallery-preview-panel");
873            addComponent(m_panel);
874        }
875    }
876
877    /**
878     * Class representing a file delete composite with a check box to mark a file as deleted.<p>
879     */
880    private class FileDeleteComposite extends VerticalLayout {
881
882        /** The default serial version UID. */
883        private static final long serialVersionUID = 1L;
884
885        /** The component width. */
886        private static final String WIDTH = "206px";
887
888        /** The data item of this file delete composite. */
889        private DataItem m_dataItem;
890
891        /**
892         * Creates a new file delete composite for a given data item.<p>
893         *
894         * @param dataItem the data item
895         */
896        public FileDeleteComposite(DataItem dataItem) {
897
898            m_dataItem = dataItem;
899            setMargin(new MarginInfo(true, true, true, false));
900            setSpacing(false);
901            setWidth(WIDTH);
902            setHeightFull();
903            Label fileSize = createDisplayFileSize();
904            addComponent(fileSize);
905            Label dimension = createDisplayDimension();
906            if (m_dataItem.isTypeImage()) {
907                addComponent(dimension);
908            }
909            if (!m_dataItem.getIsUsed().booleanValue()) {
910                CssLayout layout = new CssLayout();
911                Label labelInUse = createDisplayInUseInfo();
912                CheckBox fieldDeleteFlag = createFieldDeleteFlag();
913                layout.addComponent(labelInUse);
914                layout.addComponent(fieldDeleteFlag);
915                addComponent(layout);
916                setExpandRatio(layout, 1.0f);
917                setComponentAlignment(layout, Alignment.BOTTOM_LEFT);
918            } else if (m_dataItem.isTypeImage()) {
919                setExpandRatio(dimension, 1.0f);
920            } else {
921                setExpandRatio(fileSize, 1.0f);
922            }
923        }
924
925        /**
926         * Creates a component displaying the dimension of this editable gallery item.<p>
927         *
928         * @return the display component
929         */
930        private Label createDisplayDimension() {
931
932            return new Label(getFormattedDimension());
933        }
934
935        /**
936         * Creates a component displaying the size of this editable gallery item.<p>
937         *
938         * @return the display component
939         */
940        private Label createDisplayFileSize() {
941
942            return new Label(getFormattedFileSize());
943        }
944
945        /**
946         * Creates a component displaying whether this editable gallery item is used.<p>
947         *
948         * @return the display component
949         */
950        private Label createDisplayInUseInfo() {
951
952            String notInUse = CmsVaadinUtils.getMessageText(Messages.GUI_GALLERY_OPTIMIZE_LABEL_NOT_IN_USE_0);
953            return new Label(notInUse);
954        }
955
956        /**
957         * Creates a check box to mark an item as deleted.<p>
958         *
959         * @return the check box
960         */
961        private CheckBox createFieldDeleteFlag() {
962
963            String caption = CmsVaadinUtils.getMessageText(Messages.GUI_GALLERY_OPTIMIZE_INPUT_DELETE_UNUSED_0);
964            CheckBox field = new CheckBox(caption);
965            field.setCaptionAsHtml(true);
966            field.setWidthFull();
967            field.setEnabled(!CmsGalleryOptimizeDialog.this.isReadOnly());
968            m_dataItem.getBinder().bind(field, DataItem::getDeleteFlag, DataItem::setDeleteFlag);
969            return field;
970        }
971
972        /**
973         * Returns the dimension of this file in the case the file is an image.<p>
974         *
975         * @return the formatted dimension
976         */
977        private String getFormattedDimension() {
978
979            String imageSize = null;
980            try {
981                imageSize = getCms().readPropertyObject(
982                    m_dataItem.getResource(),
983                    CmsPropertyDefinition.PROPERTY_IMAGE_SIZE,
984                    false).getValue();
985            } catch (CmsException e) {
986                LOG.warn(e.getLocalizedMessage(), e);
987            }
988            String dimension = "? x ?";
989            if (imageSize != null) {
990                String[] tokens = imageSize.split(",");
991                if ((tokens.length == 2) && (tokens[0].length() > 2) && (tokens[1].length() > 2)) {
992                    dimension = tokens[0].substring(2) + " x " + tokens[1].substring(2);
993                }
994            }
995            return dimension;
996        }
997
998        /**
999         * Returns the size of this editable gallery item in a formatted way.<p>
1000         *
1001         * @return the formatted file size
1002         */
1003        private String getFormattedFileSize() {
1004
1005            return (m_dataItem.getResource().getLength() / 1024) + " kb";
1006        }
1007    }
1008
1009    /**
1010     * Filter handler. Keeps track of the query string with which the user
1011     * currently filters the gallery.
1012     */
1013    private class FilterHandler {
1014
1015        /** The filter query string. */
1016        private String m_query;
1017
1018        /**
1019         * Creates a new filter handler.<p>
1020         */
1021        public FilterHandler() {
1022
1023        }
1024
1025        /**
1026         * Returns the filter query string.<p>
1027         *
1028         * @return the filter query string
1029         */
1030        public String getQuery() {
1031
1032            return m_query != null ? m_query.toLowerCase() : null;
1033        }
1034
1035        /**
1036         * Sets the filter query string.<p>
1037         *
1038         * @param query the filter query string
1039         */
1040        public void setQuery(String query) {
1041
1042            m_query = query;
1043        }
1044    }
1045
1046    /**
1047     * Class representing a form composite to edit gallery item data.<p>
1048     */
1049    private class FormComposite extends VerticalLayout {
1050
1051        /** The default serial version UID. */
1052        private static final long serialVersionUID = 1L;
1053
1054        /** The data item of this form composite. */
1055        DataItem m_dataItem;
1056
1057        /**
1058         * Creates a new form composite for a given data item.<p>
1059         *
1060         * @param dataItem the data item
1061         */
1062        public FormComposite(DataItem dataItem) {
1063
1064            m_dataItem = dataItem;
1065            setSizeFull();
1066            setMargin(true);
1067            setSpacing(false);
1068            addComponent(createCompositeResourceInfo());
1069            addComponent(createDisplayResourceDate());
1070            FormLayout formLayout = new FormLayout();
1071            formLayout.setMargin(false);
1072            formLayout.setSpacing(false);
1073            formLayout.addStyleName(OpenCmsTheme.GALLERY_FORM);
1074            formLayout.addComponent(createFieldTitle());
1075            formLayout.addComponent(createFieldCopyright());
1076            formLayout.addComponent(createFieldDescription());
1077            addComponent(formLayout);
1078            setComponentAlignment(formLayout, Alignment.BOTTOM_LEFT);
1079            setExpandRatio(formLayout, 1.0f);
1080        }
1081
1082        /**
1083         * Returns a composite to display and edit resource information.<p>
1084         *
1085         * @return the component
1086         */
1087        private CmsResourceInfo createCompositeResourceInfo() {
1088
1089            CmsResourceInfo resourceInfo = new CmsResourceInfo(m_dataItem.getResource());
1090            resourceInfo.setTopLineText(m_dataItem.getName());
1091            resourceInfo.decorateTopInput();
1092            TextField field = resourceInfo.getTopInput();
1093            m_dataItem.getBinder().bind(field, DataItem::getName, DataItem::setName);
1094            CmsGwtContextMenuButton contextMenu = new CmsGwtContextMenuButton(
1095                m_dataItem.getResource().getStructureId(),
1096                new ContextMenu(m_dataItem));
1097            contextMenu.addStyleName("o-gwt-contextmenu-button-margin");
1098            resourceInfo.setButtonWidget(contextMenu);
1099            return resourceInfo;
1100        }
1101
1102        /**
1103         * Returns a component to display resource date information.<p>
1104         *
1105         * @return the component
1106         */
1107        private Label createDisplayResourceDate() {
1108
1109            String lastModified = formatDateTime(m_dataItem.getDateLastModified().longValue());
1110            String lastModifiedBy = m_dataItem.getResourceUtil().getUserLastModified();
1111            String message = CmsVaadinUtils.getMessageText(
1112                Messages.GUI_GALLERY_OPTIMIZE_LASTMODIFIED_BY_2,
1113                lastModified,
1114                lastModifiedBy);
1115            Label label = new Label(message);
1116            label.addStyleNames(ValoTheme.LABEL_LIGHT, ValoTheme.LABEL_TINY);
1117            return label;
1118        }
1119
1120        /**
1121         * Creates the copyright form field.<p>
1122         *
1123         * @return the copyright form field.
1124         */
1125        private TextField createFieldCopyright() {
1126
1127            String caption = CmsVaadinUtils.getMessageText(
1128                org.opencms.workplace.explorer.Messages.GUI_INPUT_COPYRIGHT_0);
1129            TextField field = new TextField(caption);
1130            field.setWidthFull();
1131            field.setEnabled(!CmsGalleryOptimizeDialog.this.isReadOnly());
1132            m_dataItem.getBinder().bind(field, DataItem::getCopyright, DataItem::setCopyright);
1133            return field;
1134        }
1135
1136        /**
1137         * Creates the description form field.<p>
1138         *
1139         * @return the description form field.
1140         */
1141        private TextField createFieldDescription() {
1142
1143            String caption = CmsVaadinUtils.getMessageText(Messages.GUI_GALLERY_OPTIMIZE_INPUT_DESCRIPTION_0);
1144            TextField field = new TextField(caption);
1145            field.setWidthFull();
1146            field.setEnabled(!CmsGalleryOptimizeDialog.this.isReadOnly());
1147            m_dataItem.getBinder().bind(field, DataItem::getDescription, DataItem::setDescription);
1148            return field;
1149        }
1150
1151        /**
1152         * Creates the title form field.<p>
1153         *
1154         * @return the title form field.
1155         */
1156        private TextField createFieldTitle() {
1157
1158            String caption = CmsVaadinUtils.getMessageText(org.opencms.workplace.explorer.Messages.GUI_INPUT_TITLE_0);
1159            TextField field = new TextField(caption);
1160            field.setWidthFull();
1161            field.setEnabled(!CmsGalleryOptimizeDialog.this.isReadOnly());
1162            m_dataItem.getBinder().bind(field, DataItem::getTitle, DataItem::setTitle);
1163            return field;
1164        }
1165
1166        /**
1167         * Utility function for date formatting.<p>
1168         *
1169         * @param date the date to format
1170         * @return the formatted date
1171         */
1172        private String formatDateTime(long date) {
1173
1174            return CmsDateUtil.getDateTime(
1175                new Date(date),
1176                DateFormat.SHORT,
1177                OpenCms.getWorkplaceManager().getWorkplaceLocale(getCms()));
1178        }
1179    }
1180
1181    /**
1182     * Page handler. Keeps track of the page currently selected by the user.
1183     */
1184    private class PageHandler {
1185
1186        /** The maximum number of items per page. */
1187        public static final int LIMIT = 50;
1188
1189        /** The index of the page currently selected by the user. */
1190        private int m_currentPage = 0;
1191
1192        /**
1193         * Creates a new page handler.<p>
1194         */
1195        public PageHandler() {}
1196
1197        /**
1198         * Returns the maximum number of items per page.<p>
1199         *
1200         * @return the maximum number of items per page
1201         */
1202        public int getLimit() {
1203
1204            return LIMIT;
1205        }
1206
1207        /**
1208         * Returns the number of the first item on the page currently selected.
1209         * The first item on the first page has number 1.<p>
1210         *
1211         * @return the number of the first item currently selected
1212         */
1213        public int getNumFirstItem() {
1214
1215            return (LIMIT * m_currentPage) + 1;
1216        }
1217
1218        /**
1219         * Returns the number of the last item on the page currently selected.
1220         * The number of the last item on the last page is equal to the total number of items.
1221         *
1222         * @return the number of the last item currently selected
1223         */
1224        public int getNumLastItem() {
1225
1226            int lastItem = ((LIMIT * m_currentPage) + LIMIT);
1227            int sizeItem = getSizeItem();
1228            if (lastItem > sizeItem) {
1229                lastItem = sizeItem;
1230            }
1231            return lastItem;
1232        }
1233
1234        /**
1235         * Returns the number of available pages.<p>
1236         *
1237         * @return the number of available pages
1238         */
1239        public int getNumPages() {
1240
1241            return (int)Math.ceil((double)getSizeItem() / PageHandler.LIMIT);
1242        }
1243
1244        /**
1245         * Returns the index of the first item on the page currently selected.
1246         * The index of the first item on the first page is 0.<p>
1247         *
1248         * @return the index of the first item ob the page currently selected
1249         */
1250        public int getOffset() {
1251
1252            return LIMIT * m_currentPage;
1253        }
1254
1255        /**
1256         * Returns the total number of items.<p>
1257         *
1258         * @return the total number of items
1259         */
1260        public int getSizeItem() {
1261
1262            return m_provider.size(m_filterHandler);
1263        }
1264
1265        /**
1266         * Returns whether the current data list has pages.
1267         *
1268         * @return whether has pages
1269         */
1270        public boolean hasPages() {
1271
1272            return getSizeItem() > LIMIT;
1273        }
1274
1275        /**
1276         * Sets the current page index to a given value.
1277         * The index of the first page is 0.<p>
1278         *
1279         * @param index the page index to set
1280         */
1281        public void setCurrentPage(int index) {
1282
1283            m_currentPage = index;
1284        }
1285    }
1286
1287    /**
1288     * Class representing a data provider for sorting and paging the in-memory data list.
1289     */
1290    private class Provider extends ListDataProvider<DataItem> {
1291
1292        /** The default serial version UID. */
1293        private static final long serialVersionUID = 1L;
1294
1295        /** Comparator. */
1296        final SerializableComparator<DataItem> SORT_DATE_ASCENDING = Comparator.comparing(
1297            DataItem::getDateLastModified)::compare;
1298
1299        /** Comparator. */
1300        final SerializableComparator<DataItem> SORT_DATE_DESCENDING = Comparator.comparing(
1301            DataItem::getDateLastModified).reversed()::compare;
1302
1303        /** Comparator. */
1304        final SerializableComparator<DataItem> SORT_NOCOPYRIGHT_FIRST = Comparator.comparing(
1305            DataItem::getNoCopyright)::compare;
1306
1307        /** Comparator. */
1308        final SerializableComparator<DataItem> SORT_NODESCRIPTION_FIRST = Comparator.comparing(
1309            DataItem::getNoDescription)::compare;
1310
1311        /** Comparator. */
1312        final SerializableComparator<DataItem> SORT_PATH_ASCENDING = Comparator.comparing(
1313            DataItem::getPath,
1314            String.CASE_INSENSITIVE_ORDER)::compare;
1315
1316        /** Comparator. */
1317        final SerializableComparator<DataItem> SORT_PATH_DESCENDING = Comparator.comparing(
1318            DataItem::getPath,
1319            String.CASE_INSENSITIVE_ORDER).reversed()::compare;
1320
1321        /** Comparator. */
1322        final SerializableComparator<DataItem> SORT_TITLE_ASCENDING = Comparator.comparing(
1323            DataItem::getTitle,
1324            String.CASE_INSENSITIVE_ORDER)::compare;
1325
1326        /** Comparator. */
1327        final SerializableComparator<DataItem> SORT_TITLE_DESCENDING = Comparator.comparing(
1328            DataItem::getTitle,
1329            String.CASE_INSENSITIVE_ORDER).reversed()::compare;
1330
1331        /** Comparator. */
1332        final SerializableComparator<DataItem> SORT_UNUSED_FIRST = Comparator.comparing(DataItem::getIsUsed)::compare;
1333
1334        /**
1335         * Create a new provider for a given data item list.
1336         *
1337         * @param dataItemList the data item list
1338         */
1339        public Provider(List<DataItem> dataItemList) {
1340
1341            super(dataItemList);
1342        }
1343
1344        /**
1345         * Fetches one page of the sorted, filtered in-memory data item list.<p>
1346         *
1347         * @param pageHandler the page handler containing offset and limit information
1348         * @param filterHandler the filter handler containing the actual filter query string
1349         * @return the sorted data item page
1350         */
1351        public List<DataItem> fetch(PageHandler pageHandler, FilterHandler filterHandler) {
1352
1353            SerializablePredicate<DataItem> filter = null;
1354            Query<DataItem, SerializablePredicate<DataItem>> filterQuery = composeFilterQuery(filterHandler);
1355            if (filterQuery != null) {
1356                filter = filterQuery.getFilter().orElse(null);
1357            }
1358            Query<DataItem, SerializablePredicate<DataItem>> query = new Query<DataItem, SerializablePredicate<DataItem>>(
1359                pageHandler.getOffset(),
1360                pageHandler.getLimit(),
1361                Collections.emptyList(),
1362                getSortComparator(),
1363                filter);
1364            return super.fetch(query).collect(Collectors.toList());
1365        }
1366
1367        /**
1368         * @see com.vaadin.data.provider.ListDataProvider#getItems()
1369         */
1370        @Override
1371        public List<DataItem> getItems() {
1372
1373            return (List<DataItem>)super.getItems();
1374        }
1375
1376        /**
1377         * Returns the size of the list respecting the current filter.
1378         *
1379         * @param filterHandler the filter handler
1380         * @return the size
1381         */
1382        public int size(FilterHandler filterHandler) {
1383
1384            Query<DataItem, SerializablePredicate<DataItem>> filterQuery = composeFilterQuery(filterHandler);
1385            return filterQuery == null ? getItems().size() : super.size(filterQuery);
1386        }
1387
1388        /**
1389         * Composes a provider query for a given filter handler.<p>
1390         *
1391         * @param filterHandler the given filter handler
1392         * @return the provider query
1393         */
1394        private Query<DataItem, SerializablePredicate<DataItem>> composeFilterQuery(FilterHandler filterHandler) {
1395
1396            Query<DataItem, SerializablePredicate<DataItem>> filterQuery = null;
1397            if (!CmsStringUtil.isEmptyOrWhitespaceOnly(filterHandler.getQuery())) {
1398                filterQuery = new Query<DataItem, SerializablePredicate<DataItem>>(
1399                    dataItem -> dataItem.getFilterText().contains(filterHandler.getQuery()));
1400            }
1401            return filterQuery;
1402        }
1403    }
1404
1405    /**
1406     * Utility class to handle dialog save actions. Keeps track of the changes the
1407     * user has made since opening the dialog on the one hand and the changes since
1408     * the last save action on the other.
1409     */
1410    private class SaveHandler {
1411
1412        /** Data items modified or marked deleted since opening the dialog. */
1413        Set<DataItem> m_changed = new HashSet<DataItem>();
1414
1415        /** Data items modified or marked deleted since the last save action. */
1416        Set<DataItem> m_changedCurrent = new HashSet<DataItem>();
1417
1418        /** IDs of the resources modified or marked as deleted since opening the dialog. */
1419        Set<CmsUUID> m_changedIds = new HashSet<CmsUUID>();
1420
1421        /** Resource IDs modified or marked as deleted since the last save action. */
1422        Set<CmsUUID> m_changedIdsCurrent = new HashSet<CmsUUID>();
1423
1424        /** Data items marked deleted since opening the dialog. */
1425        Set<DataItem> m_deleted = new HashSet<DataItem>();
1426
1427        /** Data items marked deleted since the last save action. */
1428        Set<DataItem> m_deletedCurrent = new HashSet<DataItem>();
1429
1430        /** Resources deleted since the last save action. */
1431        Set<CmsResource> m_deletedCurrentResource = new HashSet<CmsResource>();
1432
1433        /** Whether the user has cancelled the last save action. */
1434        boolean m_flagCancelSave = false;
1435
1436        /**
1437         * Creates a new save handler.<p>
1438         */
1439        public SaveHandler() {}
1440
1441        /**
1442         * Marks the last save action as cancelled.<p>
1443         *
1444         * @param flagCancelSave Whether to mark as cancelled
1445         */
1446        public void setFlagCancelSave(boolean flagCancelSave) {
1447
1448            m_flagCancelSave = flagCancelSave;
1449        }
1450
1451        /**
1452         * Returns the data items modified or marked deleted since the last save action.<p>
1453         *
1454         * @return the data items
1455         */
1456        Set<DataItem> getChangedCurrent() {
1457
1458            return m_changedCurrent;
1459        }
1460
1461        /**
1462         * Returns the IDs of the resources modified or marked as deleted since opening the dialog.
1463         *
1464         * @return the resource IDs
1465         */
1466        List<CmsUUID> getChangedIds() {
1467
1468            return new ArrayList<CmsUUID>(m_changedIds);
1469        }
1470
1471        /**
1472         * Returns the data items marked deleted since the last save action.<p>
1473         *
1474         * @return the data items
1475         */
1476        Set<DataItem> getDeletedCurrent() {
1477
1478            return m_deletedCurrent;
1479        }
1480
1481        /**
1482         * Returns the resources marked deleted since the last save action.<p>
1483         *
1484         * @return the resources
1485         */
1486        List<CmsResource> getDeletedCurrentResource() {
1487
1488            return new ArrayList<CmsResource>(m_deletedCurrentResource);
1489        }
1490
1491        /**
1492         * Whether data items have been marked as deleted since the last save action.<p>
1493         *
1494         * @return whether data items have been marked as deleted or not
1495         */
1496        boolean hasDeletedCurrent() {
1497
1498            return m_deletedCurrent.size() > 0;
1499        }
1500
1501        /**
1502         * Handles a save action for a given list of data items.<p>
1503         *
1504         * @param dataList the data item list
1505         */
1506        void save(List<DataItem> dataList) {
1507
1508            if (!m_flagCancelSave) {
1509                flush();
1510            }
1511            for (DataItem dataItem : dataList) {
1512                boolean dataItemHasChanges = dataItem.getBinder().hasChanges();
1513                if (dataItemHasChanges) {
1514                    try {
1515                        dataItem.getBinder().writeBean(dataItem);
1516                        m_changedCurrent.add(dataItem);
1517                        if (dataItem.getDeleteFlag().booleanValue()) {
1518                            m_deletedCurrent.add(dataItem);
1519                            m_deletedCurrentResource.add(dataItem.getResource());
1520                        } else if ((dataItem.getDeleteFlag().booleanValue() == false)
1521                            && m_deletedCurrent.contains(dataItem)) {
1522                                m_deletedCurrent.remove(dataItem);
1523                                m_deletedCurrentResource.remove(dataItem.getResource());
1524                            }
1525                    } catch (ValidationException e) {
1526                        LOG.warn(e.getLocalizedMessage(), e);
1527                    }
1528                }
1529            }
1530        }
1531
1532        /**
1533         * Secures and resets the current changes.
1534         */
1535        private void flush() {
1536
1537            m_deleted.addAll(m_deletedCurrent);
1538            m_changed.addAll(m_changedCurrent);
1539            m_changedIds.addAll(m_changedIdsCurrent);
1540            m_deletedCurrent.clear();
1541            m_changedCurrent.clear();
1542            m_changedIdsCurrent.clear();
1543            m_deletedCurrentResource.clear();
1544        }
1545    }
1546
1547    /** The sort order session attribute. */
1548    static final String GALLERY_OPTIMIZE_ATTR_SORT_ORDER = "GALLERY_OPTIMIZE_ATTR_SORT_ORDER";
1549
1550    /** Logger instance for this class. */
1551    static final Log LOG = CmsLog.getLog(CmsGalleryOptimizeDialog.class);
1552
1553    /** The default serial version UID. */
1554    private static final long serialVersionUID = 1L;
1555
1556    /** The save button. */
1557    private Button m_buttonSave;
1558
1559    /** The save and exit button. */
1560    private Button m_buttonSaveAndExit;
1561
1562    /** The UI composite representing the gallery item list header view. */
1563    private Object m_compositeDataListHeader;
1564
1565    /** The dialog context. */
1566    private I_CmsDialogContext m_context;
1567
1568    /** The UI component representing the gallery item list header view. */
1569    private VerticalLayout m_dataListHeaderView;
1570
1571    /** The UI component representing the gallery item list view. */
1572    private GridLayout m_dataListView;
1573
1574    /** The UI component representing the scrollable wrapper around the gallery item list view. */
1575    private Panel m_dataListViewScrollable;
1576
1577    /** The filter handler. */
1578    private FilterHandler m_filterHandler = new FilterHandler();
1579
1580    /** The gallery */
1581    private CmsResource m_gallery;
1582
1583    /** The lock action record for the gallery folder. */
1584    private CmsLockActionRecord m_lockActionRecord;
1585
1586    /** Localized message. */
1587    private String m_messageSortDateLastModifiedAscending;
1588
1589    /** Localized message. */
1590    private String m_messageSortDateLastModifiedDescending;
1591
1592    /** Localized message. */
1593    private String m_messageSortNoCopyrightFirst;
1594
1595    /** Localized message. */
1596    private String m_messageSortNoDescriptionFirst;
1597
1598    /** Localized message. */
1599    private String m_messageSortPathAscending;
1600
1601    /** Localized message. */
1602    private String m_messageSortPathDescending;
1603
1604    /** Localized message. */
1605    private String m_messageSortTitleAscending;
1606
1607    /** Localized message. */
1608    private String m_messageSortTitleDescending;
1609
1610    /** Localized message. */
1611    private String m_messageSortUnusedFirst;
1612
1613    /** The page handler. */
1614    private PageHandler m_pageHandler = new PageHandler();
1615
1616    /** The data provider. */
1617    private Provider m_provider;
1618
1619    /** The save handler. */
1620    private SaveHandler m_saveHandler = new SaveHandler();
1621
1622    /** Contains information about unused images. */
1623    private CssLayout m_unusedInfo = new CssLayout();
1624
1625    /**
1626     * Creates a new instance of a gallery optimize dialog.<p>
1627     *
1628     * @param context the dialog context
1629     * @param gallery the gallery folder to optimize
1630     */
1631    public CmsGalleryOptimizeDialog(I_CmsDialogContext context, CmsResource gallery) {
1632
1633        m_context = context;
1634        m_gallery = gallery;
1635        initMessages();
1636        initDialog();
1637        initLock();
1638        initEvents();
1639        m_unusedInfo.setWidth("100%");
1640        dataListLoad();
1641        displayDataListHeaderView();
1642        displayDataListViewSorted(getSessionSortOrder());
1643    }
1644
1645    /**
1646     * Returns the CMS object of this dialog.<p>
1647     *
1648     * @return the CMS object
1649     */
1650    public CmsObject getCms() {
1651
1652        return m_context.getCms();
1653    }
1654
1655    /**
1656     * Whether this dialog was opened from a read-only context.
1657     *
1658     * @see com.vaadin.ui.AbstractComponent#isReadOnly()
1659     */
1660    @Override
1661    protected boolean isReadOnly() {
1662
1663        return m_context.getCms().getRequestContext().getCurrentProject().isOnlineProject();
1664    }
1665
1666    /**
1667     * Finishes this dialog.<p>
1668     *
1669     * @param changedIds list of IDs of the changed resources
1670     */
1671    void finishDialog(List<CmsUUID> changedIds) {
1672
1673        m_context.finish(changedIds);
1674    }
1675
1676    /**
1677     * Refreshes the UI state after a list of data items has been successfully deleted.<p>
1678     *
1679     * @param dataItemList the list of deleted data items
1680     */
1681    void handleDataListDelete(List<DataItem> dataItemList) {
1682
1683        m_provider.getItems().removeAll(dataItemList);
1684        ((DataListHeaderComposite)m_compositeDataListHeader).refresh();
1685        displayDataListView(false);
1686    }
1687
1688    /**
1689     * Refreshes the UI state after a list of data items has been successfully updated.<p>
1690     *
1691     * @param dataItemList the list of updated data items
1692     * @throws CmsException thrown if reading a persisted CMS resource fails
1693     */
1694    void handleDataListUpdate(List<DataItem> dataItemList) throws CmsException {
1695
1696        for (DataItem dataItem : dataItemList) {
1697            CmsResourceFilter resourceFilter = CmsResourceFilter.IGNORE_EXPIRATION.addRequireFile();
1698            CmsResource reload = getCms().readResource(dataItem.getResource().getStructureId(), resourceFilter);
1699            dataItem.setResource(reload);
1700        }
1701        displayDataListView(false);
1702    }
1703
1704    /**
1705     * Event handler handling the dialog attach event.
1706     */
1707    void handleDialogAttach() {
1708
1709        Window window = CmsVaadinUtils.getWindow(CmsGalleryOptimizeDialog.this);
1710        window.removeAllCloseShortcuts(); // this is because Vaadin by default adds an ESC shortcut to every window
1711        // this is because the grid view unintentionally catches the focus whenever the height of the grid view
1712        // gets larger / smaller than it's containing layout, i.e., whenever the scroll-bar appears or disappears
1713        Page.getCurrent().getStyles().add(".o-gallery-grid-force-scroll { min-height: 1000px; }");
1714        m_dataListView.addStyleName("o-gallery-grid-force-scroll");
1715    }
1716
1717    /**
1718     * Event handler that discards all data edited and closes the
1719     * dialog. Asks the user for confirmation beforehand.<p>
1720     */
1721    void handleDialogCancel() {
1722
1723        if (dataListHasChanges()) {
1724            String title = CmsVaadinUtils.getMessageText(Messages.GUI_GALLERY_OPTIMIZE_CONFIRM_CANCEL_TITLE_0);
1725            String message = CmsVaadinUtils.getMessageText(Messages.GUI_GALLERY_OPTIMIZE_CONFIRM_CANCEL_0);
1726            CmsConfirmationDialog.show(title, message, new Runnable() {
1727
1728                @Override
1729                public void run() {
1730
1731                    finishDialog(new ArrayList<CmsUUID>());
1732                }
1733            });
1734        } else {
1735            finishDialog(new ArrayList<CmsUUID>());
1736        }
1737    }
1738
1739    /**
1740     * Event handler that handles the dialog detach event.
1741     */
1742    void handleDialogDetach() {
1743
1744        unlock();
1745    }
1746
1747    /**
1748     * Event handler that saves all changes and optionally closes the dialog. If there are data items
1749     * marked as deleted the user is asked for confirmation beforehand.
1750     *
1751     * @param exit whether to exit the dialog
1752     */
1753    void handleDialogSave(boolean exit) {
1754
1755        m_saveHandler.save(m_provider.getItems());
1756        if (m_saveHandler.hasDeletedCurrent()) {
1757            String title = CmsVaadinUtils.getMessageText(Messages.GUI_GALLERY_OPTIMIZE_CONFIRM_DELETE_TITLE_0);
1758            String message = CmsVaadinUtils.getMessageText(Messages.GUI_GALLERY_OPTIMIZE_CONFIRM_DELETE_0);
1759            CmsConfirmationDialog confirmationDialog = CmsConfirmationDialog.show(title, message, new Runnable() {
1760
1761                @Override
1762                public void run() {
1763
1764                    persist(exit);
1765                    m_saveHandler.setFlagCancelSave(false);
1766                    if (exit) {
1767                        finishDialog(m_saveHandler.getChangedIds());
1768                    }
1769                }
1770            }, new Runnable() {
1771
1772                @Override
1773                public void run() {
1774
1775                    m_saveHandler.setFlagCancelSave(true);
1776                }
1777
1778            });
1779            confirmationDialog.displayResourceInfo(
1780                m_saveHandler.getDeletedCurrentResource(),
1781                org.opencms.ui.Messages.GUI_SELECTED_0);
1782        } else {
1783            persist(exit);
1784            if (exit) {
1785                finishDialog(m_saveHandler.getChangedIds());
1786            }
1787        }
1788    }
1789
1790    /**
1791     * For a given gallery folder resource, creates a panel with information whether
1792     * this gallery is in use.<p>
1793     *
1794     * @return the gallery in use panel
1795     * @throws CmsException the CMS exception
1796     */
1797    private HorizontalLayout createDisplayGalleryInUse() throws CmsException {
1798
1799        String galleryTitle = getGalleryTitle();
1800        String text = CmsVaadinUtils.getMessageText(Messages.GUI_GALLERY_DIRECTLY_USED_1, galleryTitle);
1801        HorizontalLayout layout1 = new HorizontalLayout();
1802        layout1.setWidthFull();
1803        layout1.addStyleNames("v-panel", "o-error-dialog", OpenCmsTheme.GALLERY_ALERT_IN_USE);
1804        HorizontalLayout layout2 = new HorizontalLayout();
1805        layout2.setWidthUndefined();
1806        Label icon = new Label(FontOpenCms.WARNING.getHtml());
1807        icon.setContentMode(ContentMode.HTML);
1808        icon.setWidthUndefined();
1809        icon.setStyleName("o-warning-icon");
1810        Label message = new Label(text);
1811        message.setContentMode(ContentMode.HTML);
1812        message.setWidthUndefined();
1813        layout2.addComponent(icon);
1814        layout2.addComponent(message);
1815        layout2.setComponentAlignment(message, Alignment.MIDDLE_LEFT);
1816        layout1.addComponent(layout2);
1817        layout1.setComponentAlignment(layout2, Alignment.MIDDLE_CENTER);
1818        return layout1;
1819    }
1820
1821    /**
1822     * Creates a component showing a warning if this dialog was opened from the online project context.<p>
1823     *
1824     * @return the component
1825     */
1826    private HorizontalLayout createDisplayInOnlineProject() {
1827
1828        String text = CmsVaadinUtils.getMessageText(Messages.GUI_GALLERY_OPTIMIZE_LABEL_IN_ONLINE_PROJECT_0);
1829        return createNote(text);
1830    }
1831
1832    /**
1833     * Creates an info box.
1834     *
1835     * @param text the text (HTML) to display in the box
1836     * @param styles the additional CSS styles for the info box
1837     * @return the info box
1838     */
1839    private HorizontalLayout createNote(String text, String... styles) {
1840
1841        HorizontalLayout layout = new HorizontalLayout();
1842        layout.setWidthFull();
1843        layout.addStyleNames("v-panel", "o-error-dialog");
1844        layout.addStyleNames(styles);
1845        Label icon = new Label(FontOpenCms.WARNING.getHtml());
1846        icon.setContentMode(ContentMode.HTML);
1847        icon.setWidthUndefined();
1848        icon.setStyleName("o-warning-icon");
1849        Label message = new Label(text);
1850        message.setContentMode(ContentMode.HTML);
1851        message.setWidthUndefined();
1852        layout.addComponent(icon);
1853        layout.addComponent(message);
1854        layout.setComponentAlignment(message, Alignment.MIDDLE_LEFT);
1855        layout.setExpandRatio(message, 1.0f);
1856        return layout;
1857    }
1858
1859    /**
1860     * Creates a note widget to display above the list.
1861     *
1862     * @param html the HTML content
1863     * @param styles the additional CSS classes
1864     * @return the created widget
1865     */
1866    private HorizontalLayout createSimpleNote(String html, String... styles) {
1867
1868        HorizontalLayout layout1 = new HorizontalLayout();
1869        layout1.setWidthFull();
1870        layout1.addStyleNames("v-panel", "o-error-dialog");
1871        layout1.addStyleName("o-optimize-gallery-note");
1872        layout1.addStyleNames(styles);
1873        Label message = new Label(html);
1874        message.setContentMode(ContentMode.HTML);
1875        layout1.addComponent(message);
1876        return layout1;
1877    }
1878
1879    /**
1880     * Whether one of the editable gallery items has been modified by the user.<p>
1881     *
1882     * @return whether has changes
1883     */
1884    private boolean dataListHasChanges() {
1885
1886        boolean hasChanges = false;
1887        for (DataItem dataItem : m_provider.getItems()) {
1888            if (dataItem.getBinder().hasChanges()) {
1889                hasChanges = true;
1890            }
1891        }
1892        return hasChanges;
1893    }
1894
1895    /**
1896     * Loads the gallery item list.<p>
1897     */
1898    private void dataListLoad() {
1899
1900        List<DataItem> dataList = new ArrayList<DataItem>();
1901        CmsObject cms = A_CmsUI.getCmsObject();
1902        try {
1903            CmsResourceFilter resourceFilter = CmsResourceFilter.IGNORE_EXPIRATION.addRequireFile();
1904            List<CmsResource> resources = cms.readResources(cms.getSitePath(m_gallery), resourceFilter);
1905            for (CmsResource resource : resources) {
1906                DataItem dataItem = new DataItem(resource);
1907                dataList.add(dataItem);
1908            }
1909        } catch (CmsException exception) {
1910            m_context.error(exception);
1911        }
1912        m_provider = new Provider(dataList);
1913    }
1914
1915    /**
1916     * Displays the UI component representing the dialog header view.<p>
1917     */
1918    private void displayDataListHeaderView() {
1919
1920        if (isReadOnly()) {
1921            m_dataListHeaderView.addComponent(createDisplayInOnlineProject());
1922        } else {
1923            try {
1924                List<CmsRelation> relations = getCms().getRelationsForResource(m_gallery, CmsRelationFilter.SOURCES);
1925                if ((relations != null) && !relations.isEmpty()) {
1926                    m_dataListHeaderView.addComponent(createDisplayGalleryInUse());
1927                }
1928            } catch (CmsException e) {
1929                LOG.warn(e.getLocalizedMessage(), e);
1930            }
1931        }
1932        m_compositeDataListHeader = new DataListHeaderComposite();
1933
1934        m_dataListHeaderView.addComponent((Component)m_compositeDataListHeader);
1935        m_dataListHeaderView.addComponent(m_unusedInfo);
1936    }
1937
1938    /**
1939     * Displays the UI component representing the scrollable gallery item list.<p>
1940     *
1941     * @param scrollToTop whether to scroll to top after displaying the gallery item list
1942     */
1943    private void displayDataListView(boolean scrollToTop) {
1944
1945        m_dataListView.removeAllComponents();
1946        List<DataItem> dataItemList = m_provider.fetch(m_pageHandler, m_filterHandler);
1947        m_dataListView.setColumns(3);
1948        m_dataListView.setRows(dataItemList.size() + 2);
1949        m_dataListView.setColumnExpandRatio(2, 1.0f);
1950        int i = 1;
1951        Label dummy = new Label(" ");
1952        dummy.setId("scrollToTop");
1953        dummy.setHeight("0px");
1954        m_dataListView.addComponent(dummy, 0, 0);
1955        for (DataItem dataItem : dataItemList) {
1956            dataItem.getCompositeFile().removeStyleName(OpenCmsTheme.GALLERY_GRID_ROW_ODD);
1957            dataItem.getCompositeFileDelete().removeStyleName(OpenCmsTheme.GALLERY_GRID_ROW_ODD);
1958            dataItem.getCompositeForm().removeStyleName(OpenCmsTheme.GALLERY_GRID_ROW_ODD);
1959            if ((i % 2) == 0) {
1960                dataItem.getCompositeFile().addStyleName(OpenCmsTheme.GALLERY_GRID_ROW_ODD);
1961                dataItem.getCompositeFileDelete().addStyleName(OpenCmsTheme.GALLERY_GRID_ROW_ODD);
1962                dataItem.getCompositeForm().addStyleName(OpenCmsTheme.GALLERY_GRID_ROW_ODD);
1963            }
1964            m_dataListView.addComponent(dataItem.getCompositeFile(), 0, i);
1965            m_dataListView.addComponent(dataItem.getCompositeFileDelete(), 1, i);
1966            m_dataListView.addComponent(dataItem.getCompositeForm(), 2, i);
1967            i++;
1968        }
1969        updateUnusedInfo();
1970        if (scrollToTop) {
1971            m_dataListViewScrollable.setScrollTop(0);
1972        }
1973    }
1974
1975    /**
1976     * Sorts the gallery item list according to a given sort order and re-renders the
1977     * gallery item list view.<p>
1978     *
1979     * @param sortOrder the sort order
1980     */
1981    private void displayDataListViewSorted(String sortOrder) {
1982
1983        SerializableComparator<DataItem> defaultSortOrder = m_provider.SORT_PATH_ASCENDING;
1984        if (sortOrder == null) {
1985            m_provider.setSortComparator(m_provider.SORT_TITLE_ASCENDING);
1986        } else if (sortOrder == m_messageSortTitleAscending) {
1987            m_provider.setSortComparator(m_provider.SORT_TITLE_ASCENDING);
1988        } else if (sortOrder == m_messageSortTitleDescending) {
1989            m_provider.setSortComparator(m_provider.SORT_TITLE_DESCENDING);
1990        } else if (sortOrder == m_messageSortDateLastModifiedAscending) {
1991            m_provider.setSortComparator(m_provider.SORT_DATE_ASCENDING);
1992        } else if (sortOrder == m_messageSortDateLastModifiedDescending) {
1993            m_provider.setSortComparator(m_provider.SORT_DATE_DESCENDING);
1994        } else if (sortOrder == m_messageSortPathAscending) {
1995            m_provider.setSortComparator(m_provider.SORT_PATH_ASCENDING);
1996        } else if (sortOrder == m_messageSortPathDescending) {
1997            m_provider.setSortComparator(m_provider.SORT_PATH_DESCENDING);
1998        } else if (sortOrder == m_messageSortUnusedFirst) {
1999            m_provider.setSortComparator(m_provider.SORT_UNUSED_FIRST);
2000        } else if (sortOrder == m_messageSortNoCopyrightFirst) {
2001            m_provider.setSortComparator(m_provider.SORT_NOCOPYRIGHT_FIRST);
2002        } else if (sortOrder == m_messageSortNoDescriptionFirst) {
2003            m_provider.setSortComparator(m_provider.SORT_NODESCRIPTION_FIRST);
2004        } else {
2005            m_provider.setSortComparator(defaultSortOrder);
2006        }
2007        setSessionSortOrder(sortOrder);
2008        displayDataListView(true);
2009    }
2010
2011    /**
2012     * Returns the gallery title for a given gallery folder resource.<p>
2013     *
2014     * @return the title
2015     * @throws CmsException the CMS exception
2016     */
2017    private String getGalleryTitle() throws CmsException {
2018
2019        String galleryTitle = getCms().readPropertyObject(
2020            m_gallery,
2021            CmsPropertyDefinition.PROPERTY_TITLE,
2022            false).getValue();
2023        if (CmsStringUtil.isEmptyOrWhitespaceOnly(galleryTitle)) {
2024            galleryTitle = m_gallery.getName();
2025        }
2026        return galleryTitle;
2027    }
2028
2029    /**
2030     * Returns the current sort order saved in the user session with lazy initialization.<p>
2031     *
2032     * @return the sort order
2033     */
2034    private String getSessionSortOrder() {
2035
2036        WrappedSession wrappedSession = VaadinService.getCurrentRequest().getWrappedSession();
2037        String currentSortOrder = (String)wrappedSession.getAttribute(GALLERY_OPTIMIZE_ATTR_SORT_ORDER);
2038        if (currentSortOrder == null) {
2039            wrappedSession.setAttribute(GALLERY_OPTIMIZE_ATTR_SORT_ORDER, m_messageSortPathAscending);
2040        }
2041        return (String)wrappedSession.getAttribute(GALLERY_OPTIMIZE_ATTR_SORT_ORDER);
2042    }
2043
2044    /**
2045     * Initializes this dialog.<p>
2046     */
2047    private void initDialog() {
2048
2049        CmsVaadinUtils.readAndLocalizeDesign(this, CmsVaadinUtils.getWpMessagesForCurrentLocale(), null);
2050        displayResourceInfo(m_gallery);
2051        Button buttonCancel = createButtonCancel();
2052        buttonCancel.addClickListener(event -> {
2053            CmsGalleryOptimizeDialog.this.handleDialogCancel();
2054        });
2055        addButton(buttonCancel, false);
2056        m_buttonSave.setEnabled(!isReadOnly());
2057        m_buttonSaveAndExit.setEnabled(!isReadOnly());
2058    }
2059
2060    /**
2061     * Initializes the events of this dialog.<p>
2062     */
2063    private void initEvents() {
2064
2065        m_buttonSave.addClickListener(event -> {
2066            CmsGalleryOptimizeDialog.this.handleDialogSave(false);
2067        });
2068        m_buttonSaveAndExit.addClickListener(event -> {
2069            CmsGalleryOptimizeDialog.this.handleDialogSave(true);
2070        });
2071        setActionHandler(new CmsOkCancelActionHandler() {
2072
2073            private static final long serialVersionUID = 1L;
2074
2075            @Override
2076            protected void cancel() {
2077
2078                CmsGalleryOptimizeDialog.this.handleDialogCancel();
2079            }
2080
2081            @Override
2082            protected void ok() {
2083
2084                CmsGalleryOptimizeDialog.this.handleDialogSave(true);
2085            }
2086        });
2087        addAttachListener(event -> {
2088            CmsGalleryOptimizeDialog.this.handleDialogAttach();
2089        });
2090        addDetachListener(event -> {
2091            CmsGalleryOptimizeDialog.this.handleDialogDetach();
2092        });
2093    }
2094
2095    /**
2096     * Locks the gallery folder.<p>
2097     */
2098    private void initLock() {
2099
2100        try {
2101            m_lockActionRecord = CmsLockUtil.ensureLock(getCms(), m_gallery);
2102        } catch (CmsException e) {
2103            LOG.warn(e.getLocalizedMessage(), e);
2104        }
2105    }
2106
2107    /**
2108     * Initializes the localized messages of this dialog.<p>
2109     */
2110    private void initMessages() {
2111
2112        m_messageSortDateLastModifiedAscending = CmsVaadinUtils.getMessageText(
2113            Messages.GUI_GALLERY_OPTIMIZE_SORT_DATE_MODIFIED_ASCENDING_0);
2114        m_messageSortDateLastModifiedDescending = CmsVaadinUtils.getMessageText(
2115            Messages.GUI_GALLERY_OPTIMIZE_SORT_DATE_MODIFIED_DESCENDING_0);
2116        m_messageSortPathAscending = CmsVaadinUtils.getMessageText(Messages.GUI_GALLERY_OPTIMIZE_SORT_PATH_ASCENDING_0);
2117        m_messageSortPathDescending = CmsVaadinUtils.getMessageText(
2118            Messages.GUI_GALLERY_OPTIMIZE_SORT_PATH_DESCENDING_0);
2119        m_messageSortTitleAscending = CmsVaadinUtils.getMessageText(
2120            Messages.GUI_GALLERY_OPTIMIZE_SORT_TITLE_ASCENDING_0);
2121        m_messageSortTitleDescending = CmsVaadinUtils.getMessageText(
2122            Messages.GUI_GALLERY_OPTIMIZE_SORT_TITLE_DESCENDING_0);
2123        m_messageSortUnusedFirst = CmsVaadinUtils.getMessageText(Messages.GUI_GALLERY_OPTIMIZE_SORT_UNUSED_FIRST_0);
2124        m_messageSortNoCopyrightFirst = CmsVaadinUtils.getMessageText(
2125            Messages.GUI_GALLERY_OPTIMIZE_SORT_NOCOPYRIGHT_FIRST_0);
2126        m_messageSortNoDescriptionFirst = CmsVaadinUtils.getMessageText(
2127            Messages.GUI_GALLERY_OPTIMIZE_SORT_NODESCRIPTION_FIRST_0);
2128    }
2129
2130    /**
2131     * Persists all data changes that have not been saved yet. Refreshes the UI.
2132     * Informs the user about failed updates, failed renames and failed deletes.<p>
2133     *
2134     * @param exit true if we exit the dialog after saving
2135     */
2136    private void persist(boolean exit) {
2137
2138        StringBuilder errorMessageList = new StringBuilder();
2139        persistUpdateAndRename(errorMessageList);
2140        persistDelete(errorMessageList);
2141
2142        // In the embedded dialog case, using the Vaadin notifications when exiting the dialog doesn't work (the iframe
2143        // is hidden before the notifications show up).
2144        // We don't really need notifications for the successful case, but when errors occur, we send them to the
2145        // ADE notification mechanism.
2146        boolean embeddedExit = (m_context instanceof CmsEmbeddedDialogContext) && exit;
2147
2148        if (errorMessageList.length() == 0) {
2149
2150            if (!embeddedExit) {
2151                String message = CmsVaadinUtils.getMessageText(
2152                    Messages.GUI_GALLERY_OPTIMIZE_LABEL_SUCCESSFULLY_SAVED_0);
2153                Notification notification = new Notification(message, "", Notification.Type.HUMANIZED_MESSAGE);
2154                notification.setPosition(Position.TOP_CENTER);
2155                notification.show(Page.getCurrent());
2156            }
2157        } else {
2158            if (embeddedExit) {
2159                ((CmsEmbeddedDialogContext)m_context).sendNotification(true, errorMessageList.toString());
2160            } else {
2161                Notification notification = new Notification(
2162                    "",
2163                    errorMessageList.toString(),
2164                    Notification.Type.ERROR_MESSAGE);
2165                notification.setHtmlContentAllowed(true);
2166                notification.setPosition(Position.TOP_CENTER);
2167                notification.show(Page.getCurrent());
2168            }
2169        }
2170    }
2171
2172    /**
2173     * Persists all deleted gallery items.<p>
2174     *
2175     * @param errorMessageList string builder to append error messages
2176     */
2177    private void persistDelete(StringBuilder errorMessageList) {
2178
2179        List<DataItem> deleted = new ArrayList<DataItem>();
2180        for (DataItem dataItem : m_saveHandler.getDeletedCurrent()) {
2181            CmsResource resource = dataItem.getResource();
2182            try {
2183                getCms().deleteResource(getCms().getSitePath(resource), CmsResource.DELETE_PRESERVE_SIBLINGS);
2184                deleted.add(dataItem);
2185            } catch (CmsException e) {
2186                errorMessageList.append("<div>" + e.getLocalizedMessage() + "</div>");
2187                LOG.warn(e.getLocalizedMessage(), e);
2188            }
2189        }
2190        handleDataListDelete(deleted);
2191        if (m_saveHandler.hasDeletedCurrent()) {
2192            displayDataListView(true);
2193        }
2194    }
2195
2196    /**
2197     * Persists all updated and renamed gallery items.<p>
2198     *
2199     * @param errorMessageList string builder to append error messages
2200     */
2201    private void persistUpdateAndRename(StringBuilder errorMessageList) {
2202
2203        List<DataItem> updated = new ArrayList<DataItem>();
2204        for (DataItem dataItem : m_saveHandler.getChangedCurrent()) {
2205            CmsResource resource = dataItem.getResource();
2206            if (dataItem.hasChanges()) {
2207                try {
2208                    getCms().writePropertyObjects(resource, dataItem.getPropertyList());
2209                    getCms().writeResource(resource);
2210                    updated.add(dataItem);
2211                } catch (CmsException e) {
2212                    errorMessageList.append("<div>" + e.getLocalizedMessage() + "</div>");
2213                    LOG.warn(e.getLocalizedMessage(), e);
2214                }
2215            }
2216            if (dataItem.isRenamed()) {
2217                String source = getCms().getSitePath(resource);
2218                String destination = CmsStringUtil.joinPaths(CmsResource.getParentFolder(source), dataItem.getName());
2219                try {
2220                    getCms().renameResource(source, destination);
2221                    if (!updated.contains(dataItem)) {
2222                        updated.add(dataItem);
2223                    }
2224                } catch (CmsException e) {
2225                    errorMessageList.append("<div>" + e.getLocalizedMessage() + "</div>");
2226                    LOG.warn(e.getLocalizedMessage(), e);
2227                }
2228            }
2229        }
2230        try {
2231            handleDataListUpdate(updated);
2232        } catch (CmsException e) {
2233            errorMessageList.append("<div>" + e.getLocalizedMessage() + "</div>");
2234            LOG.warn(e.getLocalizedMessage(), e);
2235        }
2236    }
2237
2238    /**
2239     * Saves the selected sort order in the user session.<p>
2240     *
2241     * @param sortOrder the sort order
2242     */
2243    private void setSessionSortOrder(String sortOrder) {
2244
2245        WrappedSession wrappedSession = VaadinService.getCurrentRequest().getWrappedSession();
2246        wrappedSession.setAttribute(GALLERY_OPTIMIZE_ATTR_SORT_ORDER, sortOrder);
2247    }
2248
2249    /**
2250     * Unlocks the gallery folder.<p>
2251     */
2252    private void unlock() {
2253
2254        if (m_lockActionRecord != null) {
2255            try {
2256                getCms().unlockResource(m_gallery);
2257            } catch (CmsException e) {
2258                LOG.warn(e.getLocalizedMessage(), e);
2259            }
2260        }
2261    }
2262
2263    /**
2264     * Updates the 'unused elements' information.
2265     */
2266    private void updateUnusedInfo() {
2267
2268        m_unusedInfo.removeAllComponents();
2269        m_unusedInfo.setVisible(true);
2270        boolean isImageGallery = OpenCms.getResourceManager().matchResourceType("imagegallery", m_gallery.getTypeId());
2271        if (m_provider.getSortComparator() == m_provider.SORT_UNUSED_FIRST) {
2272            long unusedCount = m_provider.getItems().stream().filter(item -> !item.getIsUsed()).count();
2273            if (unusedCount == 0) {
2274                String text = CmsVaadinUtils.getMessageText(
2275                    isImageGallery
2276                    ? Messages.GUI_GALLERY_OPTIMIZE_NO_UNUSED_0
2277                    : Messages.GUI_GALLERY_OPTIMIZE_NO_UNUSED_DOWNLOADS_0);
2278                m_unusedInfo.addComponent(createSimpleNote(text, "o-optimize-gallery-warning"));
2279            } else {
2280                String text = CmsVaadinUtils.getMessageText(
2281                    isImageGallery
2282                    ? Messages.GUI_GALLERY_OPTIMIZE_NUM_UNUSED_1
2283                    : Messages.GUI_GALLERY_OPTIMIZE_NUM_UNUSED_DOWNLOADS_1,
2284                    unusedCount);
2285                m_unusedInfo.addComponent(createSimpleNote(text));
2286            }
2287        } else if (m_provider.getSortComparator() == m_provider.SORT_NOCOPYRIGHT_FIRST) {
2288            long noCopyright = m_provider.getItems().stream().filter(item -> !item.getNoCopyright()).count();
2289            if (noCopyright == 0) {
2290                String text = CmsVaadinUtils.getMessageText(
2291                    isImageGallery
2292                    ? Messages.GUI_GALLERY_OPTIMIZE_NO_NOCOPYRIGHT_0
2293                    : Messages.GUI_GALLERY_OPTIMIZE_NO_NOCOPYRIGHT_DOWNLOADS_0);
2294                m_unusedInfo.addComponent(createSimpleNote(text, "o-optimize-gallery-warning"));
2295            } else if (noCopyright == 1) {
2296                String text = CmsVaadinUtils.getMessageText(
2297                    isImageGallery
2298                    ? Messages.GUI_GALLERY_OPTIMIZE_ONE_NOCOPYRIGHT_0
2299                    : Messages.GUI_GALLERY_OPTIMIZE_ONE_NOCOPYRIGHT_DOWNLOADS_0);
2300                m_unusedInfo.addComponent(createSimpleNote(text));
2301            } else {
2302                String text = CmsVaadinUtils.getMessageText(
2303                    isImageGallery
2304                    ? Messages.GUI_GALLERY_OPTIMIZE_NUM_NOCOPYRIGHT_1
2305                    : Messages.GUI_GALLERY_OPTIMIZE_NUM_NOCOPYRIGHT_DOWNLOADS_1,
2306                    noCopyright);
2307                m_unusedInfo.addComponent(createSimpleNote(text));
2308            }
2309        } else if (m_provider.getSortComparator() == m_provider.SORT_NODESCRIPTION_FIRST) {
2310            long noDescription = m_provider.getItems().stream().filter(item -> !item.getNoDescription()).count();
2311            if (noDescription == 0) {
2312                String text = CmsVaadinUtils.getMessageText(
2313                    isImageGallery
2314                    ? Messages.GUI_GALLERY_OPTIMIZE_NO_NODESCRIPTION_0
2315                    : Messages.GUI_GALLERY_OPTIMIZE_NO_NODESCRIPTION_DOWNLOADS_0);
2316                m_unusedInfo.addComponent(createSimpleNote(text, "o-optimize-gallery-warning"));
2317            } else if (noDescription == 1) {
2318                String text = CmsVaadinUtils.getMessageText(
2319                    isImageGallery
2320                    ? Messages.GUI_GALLERY_OPTIMIZE_ONE_NODESCRIPTION_0
2321                    : Messages.GUI_GALLERY_OPTIMIZE_ONE_NODESCRIPTION_DOWNLOADS_0);
2322                m_unusedInfo.addComponent(createSimpleNote(text));
2323            } else {
2324                String text = CmsVaadinUtils.getMessageText(
2325                    isImageGallery
2326                    ? Messages.GUI_GALLERY_OPTIMIZE_NUM_NODESCRIPTION_1
2327                    : Messages.GUI_GALLERY_OPTIMIZE_NUM_NODESCRIPTION_DOWNLOADS_1,
2328                    noDescription);
2329
2330                m_unusedInfo.addComponent(createSimpleNote(text));
2331            }
2332        } else {
2333            m_unusedInfo.setVisible(false);
2334        }
2335
2336    }
2337
2338}