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.components;
029
030import org.opencms.file.CmsObject;
031import org.opencms.file.CmsResource;
032import org.opencms.file.CmsResourceFilter;
033import org.opencms.file.CmsVfsResourceNotFoundException;
034import org.opencms.main.CmsException;
035import org.opencms.main.CmsLog;
036import org.opencms.main.OpenCms;
037import org.opencms.security.CmsPermissionViolationException;
038import org.opencms.site.CmsSite;
039import org.opencms.ui.CmsVaadinUtils;
040import org.opencms.ui.components.CmsExtendedSiteSelector.SiteSelectorOption;
041import org.opencms.util.CmsFileUtil;
042import org.opencms.util.CmsPath;
043import org.opencms.util.CmsStringUtil;
044import org.opencms.workplace.explorer.CmsResourceUtil;
045
046import java.util.ArrayList;
047import java.util.Collections;
048import java.util.LinkedHashMap;
049import java.util.List;
050import java.util.Map;
051import java.util.Optional;
052
053import org.apache.commons.logging.Log;
054
055import com.google.common.base.Objects;
056import com.google.common.collect.ArrayListMultimap;
057import com.google.common.collect.Multimap;
058import com.vaadin.data.provider.ListDataProvider;
059import com.vaadin.ui.ComboBox;
060
061/**
062 * Site selector widget which also optionally offers subsite options.
063 **/
064public class CmsExtendedSiteSelector extends ComboBox<SiteSelectorOption> {
065
066    /**
067     * Class representing a single option.
068     */
069    public static class SiteSelectorOption {
070
071        /** The label for the option. */
072        private String m_label;
073
074        /** The path in the site (may be null). */
075        private String m_path;
076
077        /** The site root. */
078        private String m_site;
079
080        /**
081         * Creates a new instance.
082         *
083         * @param site the site root
084         * @param path the path in the site (may be null)
085         * @param label the option label
086         */
087        public SiteSelectorOption(String site, String path, String label) {
088
089            m_site = normalizePath(site);
090            m_path = normalizePath(path);
091            m_label = label;
092        }
093
094        /**
095         * @see java.lang.Object#equals(java.lang.Object)
096         */
097        @Override
098        public boolean equals(Object obj) {
099
100            if (this == obj) {
101                return true;
102            }
103            if (obj == null) {
104                return false;
105            }
106            if (getClass() != obj.getClass()) {
107                return false;
108            }
109            SiteSelectorOption other = (SiteSelectorOption)obj;
110            return Objects.equal(m_site, other.m_site) && Objects.equal(m_path, other.m_path);
111        }
112
113        /**
114         * Gets the option label.
115         *
116         * @return the option label
117         */
118        public String getLabel() {
119
120            return m_label;
121        }
122
123        /**
124         * Gets the path to jump to as a site path (may be null).
125         *
126         * @return the path to jump to
127         */
128        public String getPath() {
129
130            return m_path;
131        }
132
133        /**
134         * Gets the site root
135         *
136         * @return the site root
137         */
138        public String getSite() {
139
140            return m_site;
141        }
142
143        /**
144         * @see java.lang.Object#hashCode()
145         */
146        @Override
147        public int hashCode() {
148
149            final int prime = 31;
150            int result = 1;
151            result = (prime * result) + ((m_path == null) ? 0 : m_path.hashCode());
152            result = (prime * result) + ((m_site == null) ? 0 : m_site.hashCode());
153            return result;
154        }
155
156        /**
157         * Cuts off trailing slashes if the path is not null.
158         *
159         * @param path a path
160         * @return the normalized path
161         */
162        private String normalizePath(String path) {
163
164            if (path == null) {
165                return null;
166            }
167            return CmsFileUtil.removeTrailingSeparator(path);
168        }
169
170    }
171
172    /** Logger instance for this class. */
173    private static final Log LOG = CmsLog.getLog(CmsExtendedSiteSelector.class);
174
175    /** Serial version id. */
176    private static final long serialVersionUID = 1L;
177
178    /** Longer default page length for site selectors. */
179    public static final int LONG_PAGE_LENGTH = 20;
180
181    /** True if the options have been initialized.  */
182    private boolean m_initialized;
183
184    /** The options. */
185    private List<SiteSelectorOption> m_options = new ArrayList<>();
186
187    /**
188     * Creates a new instance.<p>
189     *
190     * To actually use the widget, the initOptions method has to be called first.
191     */
192    public CmsExtendedSiteSelector() {
193
194        super();
195
196        setDataProvider(new ListDataProvider<SiteSelectorOption>(m_options));
197        setItemCaptionGenerator(item -> item.getLabel());
198        setTextInputAllowed(true);
199        setEmptySelectionAllowed(false);
200        addAttachListener(evt -> {
201            if (!m_initialized) {
202                throw new RuntimeException(getClass().getName() + " must be initialized with initOptions");
203            }
204
205        });
206    }
207
208    /**
209     * Builds a list of site selector option that also includes subsites with the 'include in site selector' option enabled in their configuration.
210     *
211     * @param cms the current CMS context
212     * @param addSubsites true if subsite options should be added
213     * @return the the extended site selecctor options
214     */
215    public static List<CmsExtendedSiteSelector.SiteSelectorOption> getExplorerSiteSelectorOptions(
216        CmsObject cms,
217        boolean addSubsites) {
218
219        LinkedHashMap<String, String> siteOptions = CmsVaadinUtils.getAvailableSitesMap(cms);
220        List<String> subsites = new ArrayList<>(
221            OpenCms.getADEManager().getSubsitesForSiteSelector(
222                cms.getRequestContext().getCurrentProject().isOnlineProject()));
223        Collections.sort(subsites);
224        Multimap<CmsPath, CmsExtendedSiteSelector.SiteSelectorOption> subsitesForSite = ArrayListMultimap.create();
225        if (addSubsites) {
226            try {
227                CmsObject titleCms = OpenCms.initCmsObject(cms);
228                titleCms.getRequestContext().setSiteRoot("");
229                for (String subsite : subsites) {
230                    CmsSite site = OpenCms.getSiteManager().getSiteForRootPath(subsite);
231                    if ((site != null) && site.isSubsiteSelectionEnabled()) { // only use subsites that are in an actual site; also, subsite selection must be enabled on the site
232                        CmsPath siteRootPath = new CmsPath(site.getSiteRoot());
233                        if (!siteRootPath.equals(new CmsPath(subsite))) { // Don't allow the site itself as a subsite
234                            Optional<String> remainingPath = CmsStringUtil.removePrefixPath(
235                                site.getSiteRoot(),
236                                subsite);
237                            if (remainingPath.isPresent()) {
238                                try {
239                                    CmsResource subsiteRes = titleCms.readResource(
240                                        subsite,
241                                        CmsResourceFilter.ONLY_VISIBLE_NO_DELETED);
242                                    CmsResourceUtil resUtil = new CmsResourceUtil(titleCms, subsiteRes);
243                                    String title = resUtil.getTitle();
244                                    if (CmsStringUtil.isEmptyOrWhitespaceOnly(title)) {
245                                        title = subsiteRes.getName();
246                                    }
247                                    SiteSelectorOption option = new SiteSelectorOption(
248                                        site.getSiteRoot(),
249                                        remainingPath.get(),
250                                        "\u2013 " + title);
251                                    subsitesForSite.put(siteRootPath, option);
252                                } catch (CmsPermissionViolationException | CmsVfsResourceNotFoundException e) {
253                                    LOG.info(e.getLocalizedMessage(), e);
254                                } catch (CmsException e) {
255                                    LOG.warn(e.getLocalizedMessage(), e);
256                                }
257                            }
258                        }
259                    }
260                }
261            } catch (Exception e) {
262                LOG.error(e.getLocalizedMessage(), e);
263            }
264        }
265        List<SiteSelectorOption> result = new ArrayList<>();
266        for (Map.Entry<String, String> entry : siteOptions.entrySet()) {
267            result.add(new SiteSelectorOption(entry.getKey(), null, entry.getValue()));
268            result.addAll(subsitesForSite.get(new CmsPath(entry.getKey())));
269        }
270        return result;
271
272    }
273
274    /**
275     * Gets the option for the specific site root (without any subsite path).
276     *
277     * <p>If no option for the site is found, returns null.
278     *
279     * @param siteRoot the site root
280     * @return the option for the site root
281     */
282    public SiteSelectorOption getOptionForSiteRoot(String siteRoot) {
283
284        for (SiteSelectorOption option : m_options) {
285            if (Objects.equal(option.getSite(), siteRoot) && (option.getPath() == null)) {
286                return option;
287            }
288        }
289        return null;
290
291    }
292
293    /**
294     * Initializes the select options.
295     *
296     * @param cms the CMS context
297     * @param addSubsites true if subsites should be shown
298     */
299    public void initOptions(CmsObject cms, boolean addSubsites) {
300
301        List<SiteSelectorOption> options = CmsExtendedSiteSelector.getExplorerSiteSelectorOptions(cms, addSubsites);
302        m_options.clear();
303        m_options.addAll(options);
304        m_initialized = true;
305    }
306
307    /**
308     * Selects a specific site.
309     *
310     * @param siteRoot the site root
311     */
312    public void selectSite(String siteRoot) {
313
314        SiteSelectorOption option = getOptionForSiteRoot(siteRoot);
315        if (option != null) {
316            setValue(option);
317        }
318
319    }
320
321}