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.xml.containerpage;
029
030import org.opencms.ade.containerpage.shared.CmsFormatterConfig;
031import org.opencms.file.CmsObject;
032import org.opencms.file.CmsResource;
033import org.opencms.file.types.CmsResourceTypeJsp;
034import org.opencms.main.CmsException;
035import org.opencms.main.CmsLog;
036import org.opencms.main.OpenCms;
037import org.opencms.security.CmsRole;
038import org.opencms.util.CmsStringUtil;
039import org.opencms.util.CmsUUID;
040
041import java.util.ArrayList;
042import java.util.Arrays;
043import java.util.Collection;
044import java.util.Collections;
045import java.util.Comparator;
046import java.util.LinkedHashMap;
047import java.util.List;
048import java.util.Map;
049import java.util.Set;
050
051import org.apache.commons.logging.Log;
052
053import com.google.common.base.Optional;
054import com.google.common.base.Predicate;
055import com.google.common.base.Predicates;
056import com.google.common.collect.Collections2;
057import com.google.common.collect.ComparisonChain;
058import com.google.common.collect.Iterables;
059import com.google.common.collect.Maps;
060import com.google.common.collect.Sets;
061
062/**
063 * Represents a formatter configuration.<p>
064 *
065 * A formatter configuration can be either defined in the XML schema XSD of a XML content,
066 * or in a special sitemap configuration file.<p>
067 *
068 * @since 8.0.0
069 */
070public final class CmsFormatterConfiguration {
071
072    /**
073     * This class is used to sort lists of formatter beans in order of importance.<p>
074     */
075    public static class FormatterComparator implements Comparator<I_CmsFormatterBean> {
076
077        /**
078         * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
079         */
080        public int compare(I_CmsFormatterBean first, I_CmsFormatterBean second) {
081
082            return ComparisonChain.start().compare(second.getRank(), first.getRank()).compare(
083                second.isTypeFormatter() ? 1 : 0,
084                first.isTypeFormatter() ? 1 : 0).compare(second.getMinWidth(), first.getMinWidth()).result();
085        }
086    }
087
088    /**
089     * Predicate which checks whether the given formatter is a detail formatter.<p>
090     */
091    public static class IsDetail implements Predicate<I_CmsFormatterBean> {
092
093        /**
094         * @see com.google.common.base.Predicate#apply(java.lang.Object)
095         */
096        public boolean apply(I_CmsFormatterBean formatter) {
097
098            return formatter.isDetailFormatter();
099        }
100    }
101
102    /**
103     * Predicate which checks whether the given formatter is a display formatter.<p>
104     */
105    public static class IsDisplay implements Predicate<I_CmsFormatterBean> {
106
107        /**
108         * @see com.google.common.base.Predicate#apply(java.lang.Object)
109         */
110        public boolean apply(I_CmsFormatterBean formatter) {
111
112            return formatter.isDisplayFormatter();
113        }
114    }
115
116    /**
117     * Predicate to check whether the formatter is from a schema.<p>
118     */
119    public static class IsSchemaFormatter implements Predicate<I_CmsFormatterBean> {
120
121        /**
122         * @see com.google.common.base.Predicate#apply(java.lang.Object)
123         */
124        public boolean apply(I_CmsFormatterBean formatter) {
125
126            return !formatter.isFromFormatterConfigFile();
127
128        }
129    }
130
131    /**
132     * Predicate which checks whether a formatter matches the given container type or width.<p>
133     */
134    private class MatchesTypeOrWidth implements Predicate<I_CmsFormatterBean> {
135
136        /** The set of container types to match. */
137        private Set<String> m_types = Sets.newHashSet();
138
139        /** The container width. */
140        private int m_width;
141
142        /**
143         * Creates a new matcher instance.<p>
144         *
145         * @param type the container type
146         * @param width the container width
147         */
148        public MatchesTypeOrWidth(String type, int width) {
149
150            if (!CmsStringUtil.isEmptyOrWhitespaceOnly(type)) {
151                // split with comma and optionally spaces to the left/right of the comma as separator
152                m_types.addAll(Arrays.asList(type.trim().split(" *, *")));
153            }
154            m_width = width;
155        }
156
157        /**
158         * @see com.google.common.base.Predicate#apply(java.lang.Object)
159         */
160        public boolean apply(I_CmsFormatterBean formatter) {
161
162            return matchFormatter(formatter, m_types, m_width);
163        }
164    }
165
166    /** The empty formatter configuration. */
167    public static final CmsFormatterConfiguration EMPTY_CONFIGURATION = new CmsFormatterConfiguration(null, null);
168
169    /** The log instance for this class. */
170    public static final Log LOG = CmsLog.getLog(CmsFormatterConfiguration.class);
171
172    /** The container width to match all width configured formatters. */
173    public static final int MATCH_ALL_CONTAINER_WIDTH = -2;
174
175    /** CmsObject used to read the JSP resources configured in the XSD schema. */
176    private static CmsObject m_adminCms;
177
178    /** All formatters that have been added to this configuration. */
179    private List<I_CmsFormatterBean> m_allFormatters;
180
181    /** The available display formatters. */
182    private List<I_CmsFormatterBean> m_displayFormatters;
183
184    /** Cache for the searchContent option. */
185    private Map<CmsUUID, Boolean> m_searchContent = Maps.newHashMap();
186
187    /**
188     * Creates a new formatter configuration based on the given list of formatters.<p>
189     *
190     * @param cms the current users OpenCms context
191     * @param formatters the list of configured formatters
192     */
193    private CmsFormatterConfiguration(CmsObject cms, List<I_CmsFormatterBean> formatters) {
194
195        if (formatters == null) {
196            // this is needed for the empty configuration
197            m_allFormatters = Collections.emptyList();
198        } else {
199            m_allFormatters = new ArrayList<I_CmsFormatterBean>(formatters);
200        }
201        init(cms, m_adminCms);
202    }
203
204    /**
205     * Returns the formatter configuration for the current project based on the given list of formatters.<p>
206     *
207     * @param cms the current users OpenCms context, required to know which project to read the JSP from
208     * @param formatters the list of configured formatters
209     *
210     * @return the formatter configuration for the current project based on the given list of formatters
211     */
212    public static CmsFormatterConfiguration create(CmsObject cms, List<I_CmsFormatterBean> formatters) {
213
214        if ((formatters != null) && (formatters.size() > 0) && (cms != null)) {
215            return new CmsFormatterConfiguration(cms, formatters);
216        } else {
217            return EMPTY_CONFIGURATION;
218        }
219    }
220
221    /**
222     * Initialize the formatter configuration.<p>
223     *
224     * @param cms an initialized admin OpenCms user context
225     *
226     * @throws CmsException in case the initialization fails
227     */
228    public static void initialize(CmsObject cms) throws CmsException {
229
230        OpenCms.getRoleManager().checkRole(cms, CmsRole.ADMINISTRATOR);
231        try {
232            // store the Admin cms to index Cms resources
233            m_adminCms = OpenCms.initCmsObject(cms);
234            m_adminCms.getRequestContext().setSiteRoot("");
235        } catch (CmsException e) {
236            // this should never happen
237        }
238    }
239
240    /**
241     * Checks whether the given formatter bean matches the container types, width and nested flag.<p>
242     *
243     * @param formatter the formatter bean
244     * @param types the container types
245     * @param width the container width
246     *
247     * @return <code>true</code> in case the formatter matches
248     */
249    public static boolean matchFormatter(I_CmsFormatterBean formatter, Set<String> types, int width) {
250
251        if (formatter.isMatchAll()) {
252            return true;
253        }
254        if (formatter.isTypeFormatter()) {
255            return !Sets.intersection(types, formatter.getContainerTypes()).isEmpty();
256        } else {
257            return (width == MATCH_ALL_CONTAINER_WIDTH)
258                || ((formatter.getMinWidth() <= width) && (width <= formatter.getMaxWidth()));
259        }
260    }
261
262    /**
263     * Gets a list of all defined formatters.<p>
264     *
265     * @return the list of all formatters
266     */
267    public List<I_CmsFormatterBean> getAllFormatters() {
268
269        return new ArrayList<I_CmsFormatterBean>(m_allFormatters);
270    }
271
272    /**
273     * Gets the formatters which are available for the given container type and width.<p>
274     *
275     * @param containerTypes the container types (comma separated)
276     * @param containerWidth the container width
277     *
278     * @return the list of available formatters
279     */
280    public List<I_CmsFormatterBean> getAllMatchingFormatters(String containerTypes, int containerWidth) {
281
282        return new ArrayList<I_CmsFormatterBean>(
283            Collections2.filter(m_allFormatters, new MatchesTypeOrWidth(containerTypes, containerWidth)));
284
285    }
286
287    /**
288     * Selects the best matching formatter for the provided type and width from this configuration.<p>
289     *
290     * This method first tries to find the formatter for the provided container type.
291     * If this fails, it returns the width based formatter that matched the container width.<p>
292     *
293     * @param containerTypes the container types (comma separated)
294     * @param containerWidth the container width
295     *
296     * @return the matching formatter, or <code>null</code> if none was found
297     */
298    public I_CmsFormatterBean getDefaultFormatter(final String containerTypes, final int containerWidth) {
299
300        Optional<I_CmsFormatterBean> result = Iterables.tryFind(
301            m_allFormatters,
302            new MatchesTypeOrWidth(containerTypes, containerWidth));
303        return result.orNull();
304    }
305
306    /**
307     * Selects the best matching schema formatter for the provided type and width from this configuration.<p>
308     *
309     * @param containerTypes the container types (comma separated)
310     * @param containerWidth the container width
311     *
312     * @return the matching formatter, or <code>null</code> if none was found
313     */
314    public I_CmsFormatterBean getDefaultSchemaFormatter(final String containerTypes, final int containerWidth) {
315
316        Optional<I_CmsFormatterBean> result = Iterables.tryFind(
317            m_allFormatters,
318            Predicates.and(new IsSchemaFormatter(), new MatchesTypeOrWidth(containerTypes, containerWidth)));
319        return result.orNull();
320    }
321
322    /**
323     * Gets the detail formatter to use for the given type and container width.<p>
324     *
325     * @param types the container types (comma separated)
326     * @param containerWidth the container width
327     *
328     * @return the detail formatter to use
329     */
330    public I_CmsFormatterBean getDetailFormatter(String types, int containerWidth) {
331
332        // detail formatters must still match the type or width
333        Predicate<I_CmsFormatterBean> checkValidDetailFormatter = Predicates.and(
334            new MatchesTypeOrWidth(types, containerWidth),
335            new IsDetail());
336        Optional<I_CmsFormatterBean> result = Iterables.tryFind(m_allFormatters, checkValidDetailFormatter);
337        return result.orNull();
338    }
339
340    /**
341     * Gets all detail formatters.<p>
342     *
343     * @return the detail formatters
344     */
345    public Collection<I_CmsFormatterBean> getDetailFormatters() {
346
347        return Collections.<I_CmsFormatterBean> unmodifiableCollection(
348            Collections2.filter(m_allFormatters, new IsDetail()));
349    }
350
351    /**
352     * Returns the display formatter for this type.<p>
353     *
354     * @return the display formatter
355     */
356    public I_CmsFormatterBean getDisplayFormatter() {
357
358        if (!getDisplayFormatters().isEmpty()) {
359            return getDisplayFormatters().get(0);
360        }
361        return null;
362    }
363
364    /**
365     * Returns the available display formatters.<p>
366     *
367     * @return the display formatters
368     */
369    public List<I_CmsFormatterBean> getDisplayFormatters() {
370
371        if (m_displayFormatters == null) {
372            List<I_CmsFormatterBean> formatters = new ArrayList<I_CmsFormatterBean>(
373                Collections2.filter(m_allFormatters, new IsDisplay()));
374            if (formatters.size() > 1) {
375                Collections.sort(formatters, new Comparator<I_CmsFormatterBean>() {
376
377                    public int compare(I_CmsFormatterBean o1, I_CmsFormatterBean o2) {
378
379                        return o1.getRank() == o2.getRank() ? 0 : (o1.getRank() < o2.getRank() ? -1 : 1);
380                    }
381                });
382            }
383            m_displayFormatters = Collections.unmodifiableList(formatters);
384        }
385        return m_displayFormatters;
386    }
387
388    /**
389     * Returns the formatters available for selection for the given container type and width.<p>
390     *
391     * @param containerTypes the container types (comma separated)
392     * @param containerWidth the container width
393     *
394     * @return the list of available formatters
395     */
396    public Map<String, I_CmsFormatterBean> getFormatterSelection(String containerTypes, int containerWidth) {
397
398        Map<String, I_CmsFormatterBean> result = new LinkedHashMap<String, I_CmsFormatterBean>();
399        for (I_CmsFormatterBean formatter : Collections2.filter(
400            m_allFormatters,
401            new MatchesTypeOrWidth(containerTypes, containerWidth))) {
402            if (formatter.isFromFormatterConfigFile()) {
403                result.put(formatter.getId(), formatter);
404            } else {
405                result.put(
406                    CmsFormatterConfig.SCHEMA_FORMATTER_ID + formatter.getJspStructureId().toString(),
407                    formatter);
408            }
409        }
410        return result;
411    }
412
413    /**
414     * Returns the formatter from this configuration that is to be used for the preview in the ADE gallery GUI,
415     * or <code>null</code> if there is no preview formatter configured.<p>
416     *
417     * @return the formatter from this configuration that is to be used for the preview in the ADE gallery GUI,
418     * or <code>null</code> if there is no preview formatter configured
419     */
420    public I_CmsFormatterBean getPreviewFormatter() {
421
422        Optional<I_CmsFormatterBean> result;
423        result = Iterables.tryFind(m_allFormatters, new Predicate<I_CmsFormatterBean>() {
424
425            public boolean apply(I_CmsFormatterBean formatter) {
426
427                return formatter.isPreviewFormatter();
428            }
429        });
430        if (!result.isPresent()) {
431            result = Iterables.tryFind(m_allFormatters, new Predicate<I_CmsFormatterBean>() {
432
433                public boolean apply(I_CmsFormatterBean formatter) {
434
435                    if (formatter.isTypeFormatter()) {
436                        return formatter.getContainerTypes().contains(CmsFormatterBean.PREVIEW_TYPE);
437                    } else {
438                        return (formatter.getMinWidth() <= CmsFormatterBean.PREVIEW_WIDTH)
439                            && (CmsFormatterBean.PREVIEW_WIDTH <= formatter.getMaxWidth());
440                    }
441                }
442            });
443        }
444        if (!result.isPresent()) {
445            result = Iterables.tryFind(m_allFormatters, new Predicate<I_CmsFormatterBean>() {
446
447                public boolean apply(I_CmsFormatterBean formatter) {
448
449                    return !formatter.isTypeFormatter() && (formatter.getMaxWidth() >= CmsFormatterBean.PREVIEW_WIDTH);
450
451                }
452            });
453        }
454        if (!result.isPresent() && !m_allFormatters.isEmpty()) {
455            result = Optional.fromNullable(m_allFormatters.iterator().next());
456        }
457        return result.orNull();
458    }
459
460    /**
461     * Returns the provided <code>true</code> in case this configuration has a formatter
462     * for the given type / width parameters.<p>
463     *
464     * @param containerTypes the container types (comma separated)
465     * @param containerWidth the container width
466     *
467     * @return the provided <code>true</code> in case this configuration has a formatter
468     *      for the given type / width parameters.
469     */
470    public boolean hasFormatter(String containerTypes, int containerWidth) {
471
472        return getDefaultFormatter(containerTypes, containerWidth) != null;
473    }
474
475    /**
476     * Returns <code>true</code> in case there is at least one usable formatter configured in this configuration.<p>
477     *
478     * @return <code>true</code> in case there is at least one usable formatter configured in this configuration
479     */
480    public boolean hasFormatters() {
481
482        return !m_allFormatters.isEmpty();
483    }
484
485    /**
486     * Returns <code>true</code> in case this configuration contains a formatter with the
487     * provided structure id that has been configured for including the formatted content in the online search.<p>
488     *
489     * @param formatterStructureId the formatter structure id
490     *
491     * @return <code>true</code> in case this configuration contains a formatter with the
492     * provided structure id that has been configured for including the formatted content in the online search
493     */
494    public boolean isSearchContent(CmsUUID formatterStructureId) {
495
496        if (EMPTY_CONFIGURATION == this) {
497            // don't search if this is just the empty configuration
498            return false;
499        }
500        // lookup the cache
501        Boolean result = m_searchContent.get(formatterStructureId);
502        if (result == null) {
503            // result so far unknown
504            for (I_CmsFormatterBean formatter : m_allFormatters) {
505                if (formatter.getJspStructureId().equals(formatterStructureId)) {
506                    // found the match
507                    result = Boolean.valueOf(formatter.isSearchContent());
508                    // first match rules
509                    break;
510                }
511            }
512            if (result == null) {
513                // no match found, in this case dont search the content
514                result = Boolean.FALSE;
515            }
516            // store result in the cache
517            m_searchContent.put(formatterStructureId, result);
518        }
519
520        return result.booleanValue();
521    }
522
523    /**
524     * Initializes all formatters of this configuration.<p>
525     *
526     * It is also checked if the configured JSP root path exists, if not the formatter is removed
527     * as it is unusable.<p>
528     *
529     * @param userCms the current users OpenCms context, used for selecting the right project
530     * @param adminCms the Admin user context to use for reading the JSP resources
531     */
532    private void init(CmsObject userCms, CmsObject adminCms) {
533
534        List<I_CmsFormatterBean> filteredFormatters = new ArrayList<I_CmsFormatterBean>();
535        for (I_CmsFormatterBean formatter : m_allFormatters) {
536
537            if (formatter.getJspStructureId() == null) {
538                // a formatter may have been re-used so the structure id is already available
539                CmsResource res = null;
540                // first we make sure that the JSP exists at all (and also we read the UUID that way)
541                try {
542                    // first get a cms copy so we can mess up the context without modifying the original
543                    CmsObject cmsCopy = OpenCms.initCmsObject(adminCms);
544                    cmsCopy.getRequestContext().setCurrentProject(userCms.getRequestContext().getCurrentProject());
545                    // switch to the root site
546                    cmsCopy.getRequestContext().setSiteRoot("");
547                    // now read the JSP
548                    res = cmsCopy.readResource(formatter.getJspRootPath());
549                } catch (CmsException e) {
550                    //if this happens the result is null and we write a LOG error
551                }
552                if ((res == null) || !CmsResourceTypeJsp.isJsp(res)) {
553                    // the formatter must exist and it must be a JSP
554                    LOG.error(
555                        Messages.get().getBundle().key(
556                            Messages.ERR_FORMATTER_JSP_DONT_EXIST_1,
557                            formatter.getJspRootPath()));
558                } else {
559                    formatter.setJspStructureId(res.getStructureId());
560                    // res may still be null in case of failure
561                }
562            }
563
564            if (formatter.getJspStructureId() != null) {
565                filteredFormatters.add(formatter);
566            } else {
567                LOG.warn("Invalid formatter: " + formatter.getJspRootPath());
568            }
569        }
570        Collections.sort(filteredFormatters, new FormatterComparator());
571        m_allFormatters = Collections.unmodifiableList(filteredFormatters);
572    }
573
574}