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