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