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.ade.detailpage;
029
030import org.opencms.file.CmsObject;
031import org.opencms.file.CmsResource;
032import org.opencms.gwt.shared.CmsGwtConstants;
033import org.opencms.main.CmsException;
034import org.opencms.main.CmsLog;
035import org.opencms.main.OpenCms;
036import org.opencms.relations.CmsCategory;
037import org.opencms.relations.CmsCategoryService;
038import org.opencms.util.CmsPath;
039
040import java.util.ArrayList;
041import java.util.Collections;
042import java.util.Comparator;
043import java.util.HashSet;
044import java.util.List;
045import java.util.Set;
046import java.util.stream.Stream;
047
048import org.apache.commons.logging.Log;
049
050/**
051 * Filters and sorts a list of detail pages based on whether they are suitable detail pages for a fixed detail content.
052 *
053 * <p>Note: Filtering on detail page types is already handled elsewhere, so this class only handles detail pages for a single type.
054 */
055public class CmsDetailPageFilter {
056
057    /** Prefix for the category qualifier in a detail page entry. */
058    public static final String PREFIX_CATEGORY = "category:";
059
060    /** Logger instance for this class. */
061    private static final Log LOG = CmsLog.getLog(CmsDetailPageFilter.class);
062
063    /** The CMS context. */
064    private CmsObject m_cms;
065
066    /** The detail resource (either this or m_path is null). */
067    private CmsResource m_resource;
068
069    /** The categories of the detail resource (lazily initialized). */
070    private Set<CmsPath> m_categories;
071
072    /** The detail root path (either this or m_resource is null). */
073    private String m_path;
074
075    /**
076     * Creates a new instance based on an actual detail resource.
077     *
078     * @param cms the CMS context
079     * @param resource the detail resource
080     */
081    public CmsDetailPageFilter(CmsObject cms, CmsResource resource) {
082
083        try {
084            m_cms = OpenCms.initCmsObject(cms);
085            m_cms.getRequestContext().setSiteRoot("");
086            m_resource = resource;
087        } catch (CmsException e) {
088            // shouldn't happen - initCmsObject doesn't *actually* throw exceptions
089            LOG.error(e.getLocalizedMessage(), e);
090        }
091
092    }
093
094    /**
095     * Creates a new instance based on the root path of a detail resource.
096     *
097     * @param cms the CMS context
098     * @param rootPath the detail resource root path
099     */
100    public CmsDetailPageFilter(CmsObject cms, String rootPath) {
101
102        try {
103            m_cms = OpenCms.initCmsObject(cms);
104            m_cms.getRequestContext().setSiteRoot("");
105            m_path = rootPath;
106        } catch (CmsException e) {
107            // shouldn't happen
108            LOG.error(e.getLocalizedMessage(), e);
109        }
110
111    }
112
113    /**
114     *
115     * Produces a filtered and sorted result stream of candidate detail pages for the detail resource.
116     *
117     * @param infos the base list of detail page beans to filter/sort
118     * @return the result stream
119     */
120    public Stream<CmsDetailPageInfo> filterDetailPages(List<CmsDetailPageInfo> infos) {
121
122        List<CmsDetailPageInfo> infos2 = new ArrayList<>(infos);
123
124        // the sort method uses a stable sort, so detail page order will be preserved for those entries with the same sort key
125        Collections.sort(infos2, new Comparator<CmsDetailPageInfo>() {
126
127            public int compare(CmsDetailPageInfo a, CmsDetailPageInfo b) {
128
129                return Integer.compare(getSortKey(a), getSortKey(b));
130            }
131
132            private int getSortKey(CmsDetailPageInfo info) {
133
134                // sort order:
135                // 0 - qualified non-default detail pages
136                // 1 - unqualified non-default detail pages
137                // 2 - qualified default detail pages
138                // 3 - unqualified default detail pages
139                boolean defaultDetailFlag = info.getType().equals(CmsGwtConstants.DEFAULT_DETAILPAGE_TYPE);
140                boolean qualifierFlag = info.getQualifier() != null;
141                if (!defaultDetailFlag) {
142                    if (qualifierFlag) {
143                        return 0;
144                    } else {
145                        return 1;
146                    }
147                } else {
148                    if (qualifierFlag) {
149                        return 2;
150                    } else {
151                        return 3;
152                    }
153                }
154            }
155        });
156        // qualified pages shouldn't work without an unqualified fallback
157        if (!infos2.stream().anyMatch(detailPage -> detailPage.getQualifier() == null)) {
158            if ((infos2.size() != 0) && LOG.isWarnEnabled()) {
159                LOG.warn(
160                    "No unqualified detail page entries found in list - probably a configuration error: " + infos2);
161            }
162            return Collections.<CmsDetailPageInfo> emptyList().stream();
163        }
164        return infos2.stream().filter(info -> (info.getQualifier() == null) || checkQualifier(info.getQualifier()));
165    }
166
167    /**
168     * Checks that a detail page qualifier matches the detail resource.
169     *
170     * @param qualifier the qualifier to check
171     * @return true if the qualifier matches the detail resource
172     */
173    protected boolean checkQualifier(String qualifier) {
174
175        // shouldn't happen, test anyway
176        if (qualifier == null) {
177            return true;
178        }
179        qualifier = qualifier.trim();
180        if (qualifier.startsWith(PREFIX_CATEGORY)) {
181            String categoryStr = qualifier.substring(PREFIX_CATEGORY.length());
182            // use CmsPath to normalize leading/trailing slashes
183            CmsPath categoryPath = new CmsPath(categoryStr);
184            return getCategories().contains(categoryPath);
185        }
186        LOG.error("Invalid detail page qualifier: " + qualifier);
187        return false;
188    }
189
190    /**
191     * Gets the category paths, lazily initializing them first if necessary.
192     *
193     * @return the set of category paths
194     */
195    protected Set<CmsPath> getCategories() {
196
197        if (m_categories == null) {
198            m_categories = readCategories();
199        }
200        return m_categories;
201    }
202
203    /**
204     * Reads the categories for the resource.
205     *
206     * @return the categories for the resource
207     */
208    protected Set<CmsPath> readCategories() {
209
210        try {
211            List<CmsCategory> categories;
212
213            if (m_resource != null) {
214                categories = CmsCategoryService.getInstance().readResourceCategories(m_cms, m_resource);
215            } else {
216                categories = CmsCategoryService.getInstance().readResourceCategories(m_cms, m_path);
217            }
218            Set<CmsPath> result = new HashSet<>();
219            for (CmsCategory category : categories) {
220                result.add(new CmsPath(category.getPath()));
221            }
222            return result;
223        } catch (Exception e) {
224            LOG.error(e.getLocalizedMessage(), e);
225            // empty result is cached, so we do not retry
226            return Collections.emptySet();
227        }
228
229    }
230
231}