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 static 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     * Checks whether the given formatter bean matches the container types, width and nested flag.<p>
264     *
265     * @param formatter the formatter bean
266     * @param types the container types
267     * @param width the container width
268     *
269     * @return <code>true</code> in case the formatter matches
270     */
271    public static boolean matchFormatter(I_CmsFormatterBean formatter, String types, int width) {
272
273        return new MatchesTypeOrWidth(types, width).apply(formatter);
274    }
275
276    /**
277     * Gets a list of all defined formatters.<p>
278     *
279     * @return the list of all formatters
280     */
281    public List<I_CmsFormatterBean> getAllFormatters() {
282
283        return new ArrayList<I_CmsFormatterBean>(m_allFormatters);
284    }
285
286    /**
287     * Gets the formatters which are available for the given container type and width.<p>
288     *
289     * @param containerTypes the container types (comma separated)
290     * @param containerWidth the container width
291     *
292     * @return the list of available formatters
293     */
294    public List<I_CmsFormatterBean> getAllMatchingFormatters(String containerTypes, int containerWidth) {
295
296        return new ArrayList<I_CmsFormatterBean>(
297            Collections2.filter(m_allFormatters, new MatchesTypeOrWidth(containerTypes, containerWidth)));
298
299    }
300
301    /**
302     * Selects the best matching formatter for the provided type and width from this configuration.<p>
303     *
304     * This method first tries to find the formatter for the provided container type.
305     * If this fails, it returns the width based formatter that matched the container width.<p>
306     *
307     * @param containerTypes the container types (comma separated)
308     * @param containerWidth the container width
309     *
310     * @return the matching formatter, or <code>null</code> if none was found
311     */
312    public I_CmsFormatterBean getDefaultFormatter(final String containerTypes, final int containerWidth) {
313
314        Optional<I_CmsFormatterBean> result = Iterables.tryFind(
315            m_allFormatters,
316            new MatchesTypeOrWidth(containerTypes, containerWidth));
317        return result.orNull();
318    }
319
320    /**
321     * Selects the best matching schema formatter for the provided type and width from this configuration.<p>
322     *
323     * @param containerTypes the container types (comma separated)
324     * @param containerWidth the container width
325     *
326     * @return the matching formatter, or <code>null</code> if none was found
327     */
328    public I_CmsFormatterBean getDefaultSchemaFormatter(final String containerTypes, final int containerWidth) {
329
330        Optional<I_CmsFormatterBean> result = Iterables.tryFind(
331            m_allFormatters,
332            Predicates.and(new IsSchemaFormatter(), new MatchesTypeOrWidth(containerTypes, containerWidth)));
333        return result.orNull();
334    }
335
336    /**
337     * Gets the detail formatter to use for the given type and container width.<p>
338     *
339     * @param types the container types (comma separated)
340     * @param containerWidth the container width
341     *
342     * @return the detail formatter to use
343     */
344    public I_CmsFormatterBean getDetailFormatter(String types, int containerWidth) {
345
346        // detail formatters must still match the type or width
347        Predicate<I_CmsFormatterBean> checkValidDetailFormatter = Predicates.and(
348            new MatchesTypeOrWidth(types, containerWidth),
349            new IsDetail());
350        Optional<I_CmsFormatterBean> result = Iterables.tryFind(m_allFormatters, checkValidDetailFormatter);
351        return result.orNull();
352    }
353
354    /**
355     * Gets all detail formatters.<p>
356     *
357     * @return the detail formatters
358     */
359    public Collection<I_CmsFormatterBean> getDetailFormatters() {
360
361        return Collections.<I_CmsFormatterBean> unmodifiableCollection(
362            Collections2.filter(m_allFormatters, new IsDetail()));
363    }
364
365    /**
366     * Returns the display formatter for this type.<p>
367     *
368     * @return the display formatter
369     */
370    public I_CmsFormatterBean getDisplayFormatter() {
371
372        if (!getDisplayFormatters().isEmpty()) {
373            return getDisplayFormatters().get(0);
374        }
375        return null;
376    }
377
378    /**
379     * Returns the available display formatters.<p>
380     *
381     * @return the display formatters
382     */
383    public List<I_CmsFormatterBean> getDisplayFormatters() {
384
385        if (m_displayFormatters == null) {
386            List<I_CmsFormatterBean> formatters = new ArrayList<I_CmsFormatterBean>(
387                Collections2.filter(m_allFormatters, new IsDisplay()));
388            if (formatters.size() > 1) {
389                Collections.sort(formatters, new Comparator<I_CmsFormatterBean>() {
390
391                    public int compare(I_CmsFormatterBean o1, I_CmsFormatterBean o2) {
392
393                        return o1.getRank() == o2.getRank() ? 0 : (o1.getRank() < o2.getRank() ? -1 : 1);
394                    }
395                });
396            }
397            m_displayFormatters = Collections.unmodifiableList(formatters);
398        }
399        return m_displayFormatters;
400    }
401
402    /**
403     * Returns the formatters available for selection for the given container type and width.<p>
404     *
405     * @param containerTypes the container types (comma separated)
406     * @param containerWidth the container width
407     *
408     * @return the list of available formatters
409     */
410    public Map<String, I_CmsFormatterBean> getFormatterSelection(String containerTypes, int containerWidth) {
411
412        Map<String, I_CmsFormatterBean> result = new LinkedHashMap<String, I_CmsFormatterBean>();
413        for (I_CmsFormatterBean formatter : Collections2.filter(
414            m_allFormatters,
415            new MatchesTypeOrWidth(containerTypes, containerWidth))) {
416            if (formatter.isFromFormatterConfigFile()) {
417                result.put(formatter.getId(), formatter);
418            } else {
419                result.put(
420                    CmsFormatterConfig.SCHEMA_FORMATTER_ID + formatter.getJspStructureId().toString(),
421                    formatter);
422            }
423        }
424        return result;
425    }
426
427    public Map<String, I_CmsFormatterBean> getFormatterSelectionByKeyOrId(String containerTypes, int containerWidth) {
428
429        Map<String, I_CmsFormatterBean> result = new LinkedHashMap<String, I_CmsFormatterBean>();
430        for (I_CmsFormatterBean formatter : Collections2.filter(
431            m_allFormatters,
432            new MatchesTypeOrWidth(containerTypes, containerWidth))) {
433            if (formatter.isFromFormatterConfigFile()) {
434                result.put(formatter.getId(), formatter);
435                if (formatter.getKey() != null) {
436                    result.put(formatter.getKey(), formatter);
437                }
438            } else {
439                result.put(
440                    CmsFormatterConfig.SCHEMA_FORMATTER_ID + formatter.getJspStructureId().toString(),
441                    formatter);
442            }
443        }
444        return result;
445
446    }
447
448    /**
449     * Gets the list of formatters for the given key or id (also supports schema_formatter ids).
450     *
451     * @param key a formatter key or id
452     * @return the list of formatters for the given key
453     */
454    public List<I_CmsFormatterBean> getFormattersForKey(String key) {
455
456        if (key == null) {
457            return new ArrayList<>();
458        }
459        List<I_CmsFormatterBean> result = new ArrayList<>();
460
461        if (key.startsWith(CmsFormatterConfig.SCHEMA_FORMATTER_ID)) {
462            String idStr = key.substring(CmsFormatterConfig.SCHEMA_FORMATTER_ID.length());
463            try {
464                CmsUUID id = new CmsUUID(idStr);
465                for (I_CmsFormatterBean formatter : m_allFormatters) {
466                    if (!formatter.isFromFormatterConfigFile() && formatter.getJspStructureId().equals(id)) {
467                        result.add(formatter);
468                    }
469                }
470            } catch (NumberFormatException e) {
471                // ignore
472            }
473        }
474        for (I_CmsFormatterBean formatter : m_allFormatters) {
475            if ((formatter.getKey() != null) && key.equals(formatter.getKey())) {
476                result.add(formatter);
477            } else if ((formatter.getId() != null) && key.equals(formatter.getId().toString())) {
478                result.add(formatter);
479            }
480        }
481        return result;
482    }
483
484    /**
485     * Returns the formatter from this configuration that is to be used for the preview in the ADE gallery GUI,
486     * or <code>null</code> if there is no preview formatter configured.<p>
487     *
488     * @return the formatter from this configuration that is to be used for the preview in the ADE gallery GUI,
489     * or <code>null</code> if there is no preview formatter configured
490     */
491    public I_CmsFormatterBean getPreviewFormatter() {
492
493        Optional<I_CmsFormatterBean> result;
494        result = Iterables.tryFind(m_allFormatters, new Predicate<I_CmsFormatterBean>() {
495
496            public boolean apply(I_CmsFormatterBean formatter) {
497
498                return formatter.isPreviewFormatter();
499            }
500        });
501        if (!result.isPresent()) {
502            result = Iterables.tryFind(m_allFormatters, new Predicate<I_CmsFormatterBean>() {
503
504                public boolean apply(I_CmsFormatterBean formatter) {
505
506                    if (formatter.isTypeFormatter()) {
507                        return formatter.getContainerTypes().contains(CmsFormatterBean.PREVIEW_TYPE);
508                    } else {
509                        return (formatter.getMinWidth() <= CmsFormatterBean.PREVIEW_WIDTH)
510                            && (CmsFormatterBean.PREVIEW_WIDTH <= formatter.getMaxWidth());
511                    }
512                }
513            });
514        }
515        if (!result.isPresent()) {
516            result = Iterables.tryFind(m_allFormatters, new Predicate<I_CmsFormatterBean>() {
517
518                public boolean apply(I_CmsFormatterBean formatter) {
519
520                    return !formatter.isTypeFormatter() && (formatter.getMaxWidth() >= CmsFormatterBean.PREVIEW_WIDTH);
521
522                }
523            });
524        }
525        if (!result.isPresent() && !m_allFormatters.isEmpty()) {
526            result = Optional.fromNullable(m_allFormatters.iterator().next());
527        }
528        return result.orNull();
529    }
530
531    /**
532     * Returns the provided <code>true</code> in case this configuration has a formatter
533     * for the given type / width parameters.<p>
534     *
535     * @param containerTypes the container types (comma separated)
536     * @param containerWidth the container width
537     *
538     * @return the provided <code>true</code> in case this configuration has a formatter
539     *      for the given type / width parameters.
540     */
541    public boolean hasFormatter(String containerTypes, int containerWidth) {
542
543        return getDefaultFormatter(containerTypes, containerWidth) != null;
544    }
545
546    /**
547     * Returns <code>true</code> in case there is at least one usable formatter configured in this configuration.<p>
548     *
549     * @return <code>true</code> in case there is at least one usable formatter configured in this configuration
550     */
551    public boolean hasFormatters() {
552
553        return !m_allFormatters.isEmpty();
554    }
555
556    /**
557     * Returns <code>true</code> in case this configuration contains a formatter with the
558     * provided structure id that has been configured for including the formatted content in the online search.<p>
559     *
560     * @param formatterStructureId the formatter structure id
561     *
562     * @return <code>true</code> in case this configuration contains a formatter with the
563     * provided structure id that has been configured for including the formatted content in the online search
564     */
565    public boolean isSearchContent(CmsUUID formatterStructureId) {
566
567        if (EMPTY_CONFIGURATION == this) {
568            // don't search if this is just the empty configuration
569            return false;
570        }
571        // lookup the cache
572        Boolean result = m_searchContent.get(formatterStructureId);
573        if (result == null) {
574            // result so far unknown
575            for (I_CmsFormatterBean formatter : m_allFormatters) {
576                if (formatter.getJspStructureId().equals(formatterStructureId)) {
577                    // found the match
578                    result = Boolean.valueOf(formatter.isSearchContent());
579                    // first match rules
580                    break;
581                }
582            }
583            if (result == null) {
584                // no match found, in this case dont search the content
585                result = Boolean.FALSE;
586            }
587            // store result in the cache
588            m_searchContent.put(formatterStructureId, result);
589        }
590
591        return result.booleanValue();
592    }
593
594    /**
595     * Initializes all formatters of this configuration.<p>
596     *
597     * It is also checked if the configured JSP root path exists, if not the formatter is removed
598     * as it is unusable.<p>
599     *
600     * @param userCms the current users OpenCms context, used for selecting the right project
601     * @param adminCms the Admin user context to use for reading the JSP resources
602     */
603    private void init(CmsObject userCms, CmsObject adminCms) {
604
605        List<I_CmsFormatterBean> filteredFormatters = new ArrayList<I_CmsFormatterBean>();
606        for (I_CmsFormatterBean formatter : m_allFormatters) {
607
608            if (formatter.getJspStructureId() == null) {
609                // a formatter may have been re-used so the structure id is already available
610                CmsResource res = null;
611                // first we make sure that the JSP exists at all (and also we read the UUID that way)
612                try {
613                    // first get a cms copy so we can mess up the context without modifying the original
614                    CmsObject cmsCopy = OpenCms.initCmsObject(adminCms);
615                    cmsCopy.getRequestContext().setCurrentProject(userCms.getRequestContext().getCurrentProject());
616                    // switch to the root site
617                    cmsCopy.getRequestContext().setSiteRoot("");
618                    // now read the JSP
619                    res = cmsCopy.readResource(formatter.getJspRootPath());
620                } catch (CmsException e) {
621                    //if this happens the result is null and we write a LOG error
622                }
623                if ((res == null) || !CmsResourceTypeJsp.isJsp(res)) {
624                    // the formatter must exist and it must be a JSP
625                    LOG.error(
626                        Messages.get().getBundle().key(
627                            Messages.ERR_FORMATTER_JSP_DONT_EXIST_1,
628                            formatter.getJspRootPath()));
629                } else {
630                    formatter.setJspStructureId(res.getStructureId());
631                    // res may still be null in case of failure
632                }
633            }
634
635            if (formatter.getJspStructureId() != null) {
636                filteredFormatters.add(formatter);
637            } else {
638                LOG.warn("Invalid formatter: " + formatter.getJspRootPath());
639            }
640        }
641        Collections.sort(filteredFormatters, new FormatterComparator());
642        m_allFormatters = Collections.unmodifiableList(filteredFormatters);
643    }
644
645}