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.favorites;
029
030import org.opencms.file.CmsObject;
031import org.opencms.file.CmsResource;
032import org.opencms.file.CmsResourceFilter;
033import org.opencms.i18n.CmsEncoder;
034import org.opencms.main.CmsException;
035import org.opencms.main.CmsLog;
036import org.opencms.main.OpenCms;
037import org.opencms.security.CmsRole;
038import org.opencms.site.CmsSite;
039import org.opencms.ui.A_CmsUI;
040import org.opencms.ui.CmsVaadinUtils;
041import org.opencms.ui.Messages;
042import org.opencms.ui.components.CmsBasicDialog;
043import org.opencms.ui.components.CmsErrorDialog;
044import org.opencms.ui.components.CmsExtendedSiteSelector;
045import org.opencms.ui.components.CmsExtendedSiteSelector.SiteSelectorOption;
046import org.opencms.ui.components.CmsResourceIcon;
047import org.opencms.ui.components.OpenCmsTheme;
048import org.opencms.ui.components.editablegroup.CmsDefaultActionHandler;
049import org.opencms.ui.components.editablegroup.CmsEditableGroup;
050import org.opencms.ui.components.editablegroup.CmsEditableGroupButtons;
051import org.opencms.ui.components.editablegroup.I_CmsEditableGroupRow;
052import org.opencms.util.CmsStringUtil;
053import org.opencms.util.CmsUUID;
054import org.opencms.workplace.explorer.CmsResourceUtil;
055
056import java.util.ArrayList;
057import java.util.HashMap;
058import java.util.LinkedHashMap;
059import java.util.List;
060import java.util.Map;
061import java.util.Optional;
062
063import org.apache.commons.logging.Log;
064
065import com.vaadin.data.provider.ListDataProvider;
066import com.vaadin.shared.ui.ContentMode;
067import com.vaadin.ui.Button;
068import com.vaadin.ui.ComboBox;
069import com.vaadin.ui.Component;
070import com.vaadin.ui.Label;
071import com.vaadin.ui.VerticalLayout;
072import com.vaadin.ui.Window;
073
074/**
075 * Dialog which shows the list of favorites for the current user and allows them to jump to individual favorites,
076 * edit the list, or add the current location to the favorite list.
077 */
078public class CmsFavoriteDialog extends CmsBasicDialog implements CmsEditableGroup.I_RowBuilder {
079
080    /**
081     * Action handler that saves favorites after every change.
082     */
083    public class SaveAfterChangeActionHandler extends CmsDefaultActionHandler {
084
085        /**
086         * Creates a new instance.
087         *
088         * @param row the row
089         */
090        @SuppressWarnings("synthetic-access")
091        public SaveAfterChangeActionHandler(I_CmsEditableGroupRow row) {
092
093            super(CmsFavoriteDialog.this.m_group, row);
094        }
095
096        /**
097         * @see org.opencms.ui.components.editablegroup.CmsDefaultActionHandler#onAdd()
098         */
099        @Override
100        public void onAdd() {
101
102            super.onAdd();
103            doSave();
104
105        }
106
107        /**
108         * @see org.opencms.ui.components.editablegroup.CmsDefaultActionHandler#onDelete()
109         */
110        @Override
111        public void onDelete() {
112
113            super.onDelete();
114            doSave();
115
116        }
117
118        /**
119         * @see org.opencms.ui.components.editablegroup.CmsDefaultActionHandler#onDown()
120         */
121        @Override
122        public void onDown() {
123
124            super.onDown();
125            doSave();
126
127        }
128
129        /**
130         * @see org.opencms.ui.components.editablegroup.CmsDefaultActionHandler#onEdit()
131         */
132        @Override
133        public void onEdit() {
134
135            Window window = CmsBasicDialog.prepareWindow(DialogWidth.narrow);
136            window.setCaption(CmsVaadinUtils.getMessageText(org.opencms.ui.Messages.GUI_FAVORITES_EDIT_TITLE_0));
137            CmsFavInfo info = (CmsFavInfo)m_row.getComponent();
138
139            CmsEditFavoriteDialog dialog = new CmsEditFavoriteDialog(info, entry -> {
140                info.setEntry(entry);
141                doSave();
142                initEntries(getEntries());
143            });
144            window.setContent(dialog);
145            A_CmsUI.get().addWindow(window);
146
147        }
148
149        /**
150         * @see org.opencms.ui.components.editablegroup.CmsDefaultActionHandler#onUp()
151         */
152        @Override
153        public void onUp() {
154
155            super.onUp();
156            doSave();
157        }
158
159    }
160
161    /**
162     * Handles changes in empty/not empty state by hiding or displaying a message.
163     */
164    class EmptyHandler implements CmsEditableGroup.I_EmptyHandler {
165
166        /** The group. */
167        private CmsEditableGroup m_groupForHandler;
168
169        /** The placeholder. */
170        private Label m_placeholder;
171
172        /**
173         * @see org.opencms.ui.components.editablegroup.CmsEditableGroup.I_EmptyHandler#init(org.opencms.ui.components.editablegroup.CmsEditableGroup)
174         */
175        public void init(CmsEditableGroup group) {
176
177            String message = CmsVaadinUtils.getMessageText(Messages.GUI_FAVORITES_EMPTY_LIST_PLACEHOLDER_0);
178            m_groupForHandler = group;
179            m_placeholder = new Label();
180            m_placeholder.setContentMode(ContentMode.HTML);
181            String spacer = "<div></div>";
182            String content = "<div>" + CmsEncoder.escapeXml(message) + "</div>";
183            m_placeholder.setValue(spacer + content + spacer);
184            m_placeholder.addStyleName(OpenCmsTheme.BOOKMARKS_PLACEHOLDER);
185            m_placeholder.setHeight("100%");
186
187        }
188
189        /**
190         * @see org.opencms.ui.components.editablegroup.CmsEditableGroup.I_EmptyHandler#setEmpty(boolean)
191         */
192        public void setEmpty(boolean empty) {
193
194            if (!m_placeholder.isAttached()) {
195                m_groupForHandler.getContainer().addComponent(m_placeholder);
196                m_groupForHandler.getContainer().setExpandRatio(m_placeholder, 1.0f);
197            }
198            m_groupForHandler.getContainer().setHeight(empty ? "100%" : null);
199            m_placeholder.setVisible(empty);
200        }
201
202    }
203
204    /** Logger instance for this class. */
205    private static final Log LOG = CmsLog.getLog(CmsFavoriteDialog.class);
206
207    /** Serial version id. */
208    private static final long serialVersionUID = 1L;
209
210    /** The Add button. */
211    private Button m_addButton;
212
213    /** The Cancel button. */
214    private Button m_cancelButton;
215
216    /** The favorite context. */
217    private I_CmsFavoriteContext m_context;
218
219    /** Current favorite location. */
220    private Optional<CmsFavoriteEntry> m_currentLocation;
221
222    /** The container layout for the favorite widgets. */
223    private VerticalLayout m_favContainer;
224
225    /** Load/save handler for favorites. */
226    private CmsFavoriteDAO m_favDao;
227
228    /** The group for the favorite widgets. */
229    private CmsEditableGroup m_group;
230
231    /** Project selector. */
232    private ComboBox<CmsUUID> m_projectBox;
233
234    /** Label for project selector. */
235    private Label m_projectBoxLabel;
236
237    /** Map of project labels. */
238    private Map<CmsUUID, String> m_projectLabels = new HashMap<>();
239
240    /** Site selector. */
241    private CmsExtendedSiteSelector m_siteBox;
242
243    /** Site labels. */
244    private Map<String, String> m_siteLabels;
245
246    /**
247     * Creates a new dialog instance.
248     *
249     * @param context the favorite context
250     * @param favDao the favorite load/save handler
251     *
252     * @throws CmsException if something goes wrong
253     */
254    public CmsFavoriteDialog(I_CmsFavoriteContext context, CmsFavoriteDAO favDao)
255    throws CmsException {
256
257        super();
258        m_favDao = favDao;
259        m_siteLabels = CmsVaadinUtils.getAvailableSitesMap(A_CmsUI.getCmsObject());
260        m_context = context;
261        context.setDialog(this);
262        CmsVaadinUtils.readAndLocalizeDesign(this, CmsVaadinUtils.getWpMessagesForCurrentLocale(), null);
263        List<CmsFavoriteEntry> entries = m_favDao.loadFavorites();
264        m_cancelButton.addClickListener(evt -> m_context.close());
265        m_favContainer.addLayoutClickListener(evt -> {
266            CmsFavoriteEntry entry = getEntry(evt.getChildComponent());
267            if (entry != null) { // may be null when user double clicks on delete icon
268                m_context.openFavorite(entry);
269            }
270        });
271        m_currentLocation = context.getFavoriteForCurrentLocation();
272        m_addButton.setEnabled(m_currentLocation.isPresent());
273        m_addButton.setCaption(CmsVaadinUtils.getMessageText(Messages.GUI_FAVORITES_ADD_BUTTON_0));
274        m_addButton.addClickListener(evt -> onClickAdd());
275        initEntries(entries);
276        CmsObject cms = A_CmsUI.getCmsObject();
277        m_siteBox.initOptions(cms, true);
278        SiteSelectorOption option = m_siteBox.getOptionForSiteRoot(cms.getRequestContext().getSiteRoot());
279        m_siteBox.setValue(
280            option != null ? option : new SiteSelectorOption(cms.getRequestContext().getSiteRoot(), null, null));
281        m_siteBox.addValueChangeListener(evt -> {
282            if (!evt.getOldValue().equals(evt.getValue())) {
283                m_context.changeSite(evt.getValue());
284            }
285        });
286        m_siteBox.setPageLength(CmsExtendedSiteSelector.LONG_PAGE_LENGTH);
287        if (OpenCms.getRoleManager().hasRole(cms, CmsRole.WORKPLACE_USER)) {
288            LinkedHashMap<CmsUUID, String> projects = CmsVaadinUtils.getProjectsMap(cms);
289            m_projectBox.setDataProvider(new ListDataProvider<>(projects.keySet()));
290            m_projectBox.setItemCaptionGenerator(project -> projects.get(project));
291            m_projectBox.setEmptySelectionAllowed(false);
292            m_projectBox.setValue(cms.getRequestContext().getCurrentProject().getId());
293            m_projectBox.addValueChangeListener(evt -> {
294                if (!evt.getOldValue().equals(evt.getValue())) {
295                    m_context.changeProject(evt.getValue());
296                }
297            });
298        } else {
299            m_projectBox.setVisible(false);
300            m_projectBoxLabel.setVisible(false);
301        }
302    }
303
304    /**
305     * @see org.opencms.ui.components.editablegroup.CmsEditableGroup.I_RowBuilder#buildRow(org.opencms.ui.components.editablegroup.CmsEditableGroup, com.vaadin.ui.Component)
306     */
307    public CmsFavInfo buildRow(CmsEditableGroup group, Component component) {
308
309        CmsFavInfo info = (CmsFavInfo)component;
310        CmsEditableGroupButtons buttons = new CmsEditableGroupButtons(new SaveAfterChangeActionHandler(info));
311        info.setButtons(buttons);
312        return info;
313    }
314
315    /**
316     * Saves the list of currently displayed favorites.
317     */
318    protected void doSave() {
319
320        List<CmsFavoriteEntry> entries = getEntries();
321        try {
322            m_favDao.saveFavorites(entries);
323        } catch (Exception e) {
324            CmsErrorDialog.showErrorDialog(e);
325        }
326    }
327
328    /**
329     * @see org.opencms.ui.components.CmsBasicDialog#enableMaxHeight()
330     */
331    @Override
332    protected void enableMaxHeight() {
333
334        // do nothing here, we want to disable the max height mechanism
335    }
336
337    /**
338     * Initializes the bookmark widgets.
339     *
340     * @param entries the list of bookmark entries
341     */
342    protected void initEntries(List<CmsFavoriteEntry> entries) {
343
344        m_favContainer.removeAllComponents();
345        m_group = new CmsEditableGroup(m_favContainer, null, new EmptyHandler());
346        m_group.setEditEnabled(true);
347        m_group.setAddButtonVisible(false);
348        m_group.setRowBuilder(this);
349        m_group.init();
350
351        for (CmsFavoriteEntry favEntry : entries) {
352            Component favInfo;
353            try {
354                favInfo = createFavInfo(favEntry);
355                m_group.addRow(favInfo);
356            } catch (CmsException e) {
357                LOG.warn(e.getLocalizedMessage(), e);
358            }
359
360        }
361    }
362
363    /**
364     * Gets the favorite entries corresponding to the currently displayed favorite widgets.
365     *
366     * @return the list of favorite entries
367     */
368    List<CmsFavoriteEntry> getEntries() {
369
370        List<CmsFavoriteEntry> result = new ArrayList<>();
371        for (I_CmsEditableGroupRow row : m_group.getRows()) {
372            CmsFavoriteEntry entry = ((CmsFavInfo)row).getEntry();
373            result.add(entry);
374        }
375        return result;
376    }
377
378    /**
379     * Creates a favorite widget for a favorite entry.
380     *
381     * @param entry the favorite entry
382     * @return the favorite widget
383     *
384     * @throws CmsException if something goes wrong
385     */
386    private CmsFavInfo createFavInfo(CmsFavoriteEntry entry) throws CmsException {
387
388        String title = "";
389        String subtitle = "";
390        CmsFavInfo result = new CmsFavInfo(entry);
391        CmsObject cms = A_CmsUI.getCmsObject();
392        String project = getProject(cms, entry);
393        String site = getSite(cms, entry);
394        CmsResource resource = null;
395        try {
396            CmsUUID idToLoad = entry.getDetailId() != null ? entry.getDetailId() : entry.getStructureId();
397            resource = cms.readResource(idToLoad, CmsResourceFilter.IGNORE_EXPIRATION.addRequireVisible());
398            CmsResourceUtil resutil = new CmsResourceUtil(cms, resource);
399            if (!CmsStringUtil.isEmptyOrWhitespaceOnly(entry.getCustomTitle())) {
400                title = entry.getCustomTitle();
401            } else {
402                switch (entry.getType()) {
403                    case explorerFolder:
404                    default:
405                        title = CmsStringUtil.isEmpty(resutil.getTitle())
406                        ? CmsResource.getName(resource.getRootPath())
407                        : resutil.getTitle();
408                        break;
409                    case page:
410                        title = resutil.getTitle();
411                        break;
412                }
413            }
414            subtitle = resource.getRootPath();
415            CmsResourceIcon icon = result.getResourceIcon();
416            icon.initContent(resutil, CmsResource.STATE_UNCHANGED, false, false);
417        } catch (CmsException e) {
418            LOG.warn(e.getLocalizedMessage(), e);
419        }
420        result.setResource(resource);
421        result.getTopLine().setValue(title);
422        result.getBottomLine().setValue(subtitle);
423        result.getProjectLabel().setValue(project);
424        result.getSiteLabel().setValue(site);
425
426        return result;
427
428    }
429
430    /**
431     * Gets the favorite entry for a given row.
432     *
433     * @param row the widget used to display the favorite
434     * @return the favorite entry for the widget
435     */
436    private CmsFavoriteEntry getEntry(Component row) {
437
438        if (row instanceof CmsFavInfo) {
439
440            return ((CmsFavInfo)row).getEntry();
441
442        }
443        return null;
444
445    }
446
447    /**
448     * Gets the project name for a favorite entry.
449     *
450     * @param cms the CMS context
451     * @param entry the favorite entry
452     * @return the project name for the favorite entry
453     * @throws CmsException if something goes wrong
454     */
455    private String getProject(CmsObject cms, CmsFavoriteEntry entry) throws CmsException {
456
457        String result = m_projectLabels.get(entry.getProjectId());
458        if (result == null) {
459            result = cms.readProject(entry.getProjectId()).getName();
460            m_projectLabels.put(entry.getProjectId(), result);
461        }
462        return result;
463    }
464
465    /**
466     * Gets the site label for the entry.
467     *
468     * @param cms the current CMS context
469     * @param entry the entry
470     * @return the site label for the entry
471     */
472    private String getSite(CmsObject cms, CmsFavoriteEntry entry) {
473
474        CmsSite site = OpenCms.getSiteManager().getSiteForRootPath(entry.getSiteRoot());
475        if (m_siteLabels.containsKey(entry.getSiteRoot())) {
476            return m_siteLabels.get(entry.getSiteRoot());
477        }
478        String result = entry.getSiteRoot();
479        if (site != null) {
480            if (!CmsStringUtil.isEmpty(site.getTitle())) {
481                result = site.getTitle();
482            }
483        }
484        return result;
485    }
486
487    /**
488     * The click handler for the add button.
489     */
490    private void onClickAdd() {
491
492        if (m_currentLocation.isPresent()) {
493            CmsFavoriteEntry entry = m_currentLocation.get();
494            List<CmsFavoriteEntry> entries = getEntries();
495            entries.add(entry);
496            try {
497                m_favDao.saveFavorites(entries);
498            } catch (Exception e) {
499                CmsErrorDialog.showErrorDialog(e);
500            }
501            m_context.close();
502        }
503    }
504
505}