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.jsp.util;
029
030import org.opencms.acacia.shared.CmsTabInfo;
031import org.opencms.ade.contenteditor.CmsContentTypeVisitor;
032import org.opencms.ade.contenteditor.CmsWidgetUtil;
033import org.opencms.ade.contenteditor.CmsWidgetUtil.WidgetInfo;
034import org.opencms.file.CmsObject;
035import org.opencms.i18n.CmsMessages;
036import org.opencms.i18n.CmsMultiMessages;
037import org.opencms.jsp.util.I_CmsFormatterInfo.ResolveMode;
038import org.opencms.main.CmsLog;
039import org.opencms.main.OpenCms;
040import org.opencms.util.CmsMacroResolver;
041import org.opencms.util.CmsStringUtil;
042import org.opencms.widgets.A_CmsWidget;
043import org.opencms.widgets.CmsSelectWidgetOption;
044import org.opencms.xml.CmsXmlContentDefinition;
045import org.opencms.xml.content.CmsDefaultXmlContentHandler;
046import org.opencms.xml.content.I_CmsXmlContentHandler;
047import org.opencms.xml.types.CmsXmlNestedContentDefinition;
048import org.opencms.xml.types.I_CmsXmlSchemaType;
049
050import java.util.ArrayList;
051import java.util.Collection;
052import java.util.HashMap;
053import java.util.LinkedHashMap;
054import java.util.List;
055import java.util.Locale;
056import java.util.Map;
057import java.util.Set;
058import java.util.function.Function;
059import java.util.stream.Collectors;
060
061import org.apache.commons.logging.Log;
062
063/**
064 * Bean for accessing XML content schema information from JSPs.
065 */
066public class CmsSchemaInfo {
067
068    /**
069     * Represents information about a single field in a content schema.
070     */
071    public class Field implements I_CmsInfoWrapper {
072
073        /** The nested fields. */
074        private LinkedHashMap<String, Field> m_children = new LinkedHashMap<>();
075
076        /** The content definition (may be null). */
077        private CmsXmlContentDefinition m_contentDefinition;
078
079        /** The field. */
080        private I_CmsXmlSchemaType m_field;
081
082        /** The path of the field. */
083        private String m_path = "";
084
085        /** The widget information. */
086        private WidgetInfo m_widgetInfo;
087
088        /**
089         * Creates a new instance for the root level of a schema.
090         *
091         * @param contentDef the content definition
092         */
093        public Field(CmsXmlContentDefinition contentDef) {
094
095            m_contentDefinition = contentDef;
096            m_path = "";
097            processContentDefinition(m_contentDefinition);
098        }
099
100        /**
101         * Creates a new instance.
102         *
103         * @param field the schema type for the field
104         * @param path the path of the field
105         */
106        public Field(I_CmsXmlSchemaType field, String path) {
107
108            m_path = path;
109            m_field = field;
110            if (field instanceof CmsXmlNestedContentDefinition) {
111                CmsXmlNestedContentDefinition nestedDef = (CmsXmlNestedContentDefinition)field;
112                m_contentDefinition = nestedDef.getNestedContentDefinition();
113                processContentDefinition(m_contentDefinition);
114            }
115        }
116
117        /**
118         * Gets the nested fields, if any.
119         *
120         *
121         * @return the nested fields
122         */
123        public Collection<Field> getChildren() {
124
125            return m_children.values();
126        }
127
128        /**
129         * Gets the content definition.
130         *
131         * @return the content definition
132         */
133        public CmsXmlContentDefinition getContentDefinition() {
134
135            return m_contentDefinition;
136        }
137
138        /**
139         * Gets the default value.
140         *
141         * @return the default value
142         */
143        @SuppressWarnings("synthetic-access")
144        public String getDefaultValue() {
145
146            if (m_field == null) {
147                // root node
148                return null;
149            }
150            return m_root.getContentDefinition().getContentHandler().getDefault(m_cms, null, m_field, m_path, m_locale);
151        }
152
153        /**
154         * Gets the description.
155         *
156         * @return the description
157         */
158        public String getDescription() {
159
160            return getDescription(ResolveMode.text, m_cms.getRequestContext().getLocale());
161        }
162
163        /**
164         * Gets the localized description
165         * 
166         * @param locale the locale
167         * @return the description for the given locale
168         */
169        public String getDescription(Locale locale) {
170
171            return getDescription(ResolveMode.text, locale);
172
173        }
174
175        /**
176         * Gets the description key.
177         *
178         * @return the description key
179         */
180        public String getDescriptionKey() {
181
182            return getDescription(ResolveMode.key, m_cms.getRequestContext().getLocale());
183        }
184
185        /**
186         * Gets the raw configured description.
187         *
188         * @return the raw configured description
189         */
190        public String getDescriptionRaw() {
191
192            return getDescription(ResolveMode.raw, m_cms.getRequestContext().getLocale());
193        }
194
195        /**
196         * Gets the display name.
197         *
198         * @return the display name
199         */
200        public String getDisplayName() {
201
202            return getDisplayName(ResolveMode.text);
203        }
204
205        /**
206         * Gets the display name key.
207         *
208         * @return the display name key
209         */
210        public String getDisplayNameKey() {
211
212            return getDisplayName(ResolveMode.key);
213        }
214
215        /**
216         * Gets the raw configured string for the display name.
217         *
218         * @return the raw display name
219         */
220        public String getDisplayNameRaw() {
221
222            return getDisplayName(ResolveMode.raw);
223
224        }
225
226        /**
227         * Checks if this is a choice type.
228         *
229         * @return true if this is a choice type
230         */
231        public boolean getIsChoice() {
232
233            return m_field.isChoiceType();
234        }
235
236        /**
237         * Checks if this is a nested content type.
238         *
239         * @return true if this is a nested content type
240         */
241        public boolean getIsNestedContent() {
242
243            return m_contentDefinition != null;
244        }
245
246        /**
247         * Gets the maximum number of occurrences.
248         *
249         * @return the maximum number of occurrences
250         */
251        public int getMaxOccurs() {
252
253            return m_field.getMaxOccurs();
254        }
255
256        /**
257         * Gets the minimum number of occurrences.
258         *
259         * @return the minimum number of occurrences
260         */
261        public int getMinOccurs() {
262
263            return m_field.getMinOccurs();
264        }
265
266        /**
267         * Gets the field name.
268         *
269         * @return the name
270         */
271        public String getName() {
272
273            if (m_field == null) {
274                return null;
275            }
276            return m_field.getName();
277        }
278
279        /**
280         * Tries to interpret the widget configuration as a select option configuration and returns the list of select options if this succeeds, and null otherwise.
281         *
282         * @return the list of parsed select options, or null if the widget configuration couldn't be interpreted that way
283         */
284        @SuppressWarnings({"synthetic-access"})
285        public List<CmsSelectWidgetOption> getParsedSelectOptions() {
286
287            String widgetConfig = getWidgetConfig();
288            if (CmsStringUtil.isEmptyOrWhitespaceOnly(widgetConfig)) {
289                // passing an empty/null configuration to parseOptions would result in an empty list, not null, and we want null here
290                return null;
291            }
292            try {
293                List<CmsSelectWidgetOption> options = org.opencms.widgets.CmsSelectWidgetOption.parseOptions(
294                    widgetConfig);
295                List<CmsSelectWidgetOption> result = new ArrayList<>();
296                Set<String> values = options.stream().map(option -> option.getValue()).collect(Collectors.toSet());
297                String defaultValue = getDefaultValue();
298                Locale locale = m_cms.getRequestContext().getLocale();
299                if (CmsStringUtil.isEmptyOrWhitespaceOnly(defaultValue) || !values.contains(defaultValue)) {
300                    CmsSelectWidgetOption noValue = new CmsSelectWidgetOption(
301                        "",
302                        true,
303                        org.opencms.gwt.Messages.get().getBundle(locale).key(
304                            org.opencms.gwt.Messages.GUI_SELECTBOX_EMPTY_SELECTION_0));
305                    result.add(noValue);
306                }
307
308                result.addAll(options);
309                return result;
310            } catch (Exception e) {
311                LOG.info(e.getLocalizedMessage(), e);
312                return null;
313            }
314        }
315
316        /**
317         * Gets the field type.
318         *
319         * @return the type
320         */
321        public String getType() {
322
323            return m_field.getTypeName();
324        }
325
326        /**
327         * Gets the validation error message.
328         *
329         * @return the validation error message
330         */
331        @SuppressWarnings("synthetic-access")
332        public String getValidationError() {
333
334            if (m_field == null) {
335                return null;
336            }
337            I_CmsXmlContentHandler handler = m_field.getContentDefinition().getContentHandler();
338            if (!(handler instanceof CmsDefaultXmlContentHandler)) {
339                return null;
340            }
341            CmsDefaultXmlContentHandler defaultHandler = (CmsDefaultXmlContentHandler)handler;
342            return defaultHandler.getValidationWarningOrErrorMessage(m_cms, m_locale, m_field.getName(), false, false);
343        }
344
345        /**
346         * Gets the validation error localization key.
347         *
348         * @return the validation error localization key
349         */
350        @SuppressWarnings("synthetic-access")
351        public String getValidationErrorKey() {
352
353            if (m_field == null) {
354                return null;
355            }
356            I_CmsXmlContentHandler handler = m_field.getContentDefinition().getContentHandler();
357            if (!(handler instanceof CmsDefaultXmlContentHandler)) {
358                return null;
359            }
360            CmsDefaultXmlContentHandler defaultHandler = (CmsDefaultXmlContentHandler)handler;
361            return defaultHandler.getValidationWarningOrErrorMessage(m_cms, m_locale, m_field.getName(), false, true);
362        }
363
364        /**
365         * Gets the validation warning message.
366         *
367         * @return the validation warning message
368         */
369        @SuppressWarnings({"synthetic-access"})
370        public String getValidationWarning() {
371
372            if (m_field == null) {
373                return null;
374            }
375            I_CmsXmlContentHandler handler = m_field.getContentDefinition().getContentHandler();
376            if (!(handler instanceof CmsDefaultXmlContentHandler)) {
377                return null;
378            }
379            CmsDefaultXmlContentHandler defaultHandler = (CmsDefaultXmlContentHandler)handler;
380            return defaultHandler.getValidationWarningOrErrorMessage(m_cms, m_locale, m_field.getName(), true, false);
381        }
382
383        /**
384         * Gets the validation warning message key.
385         *
386         * @return the validation warning message key
387         */
388        @SuppressWarnings("synthetic-access")
389        public String getValidationWarningKey() {
390
391            if (m_field == null) {
392                return null;
393            }
394            I_CmsXmlContentHandler handler = m_field.getContentDefinition().getContentHandler();
395            if (!(handler instanceof CmsDefaultXmlContentHandler)) {
396                return null;
397            }
398            CmsDefaultXmlContentHandler defaultHandler = (CmsDefaultXmlContentHandler)handler;
399            return defaultHandler.getValidationWarningOrErrorMessage(m_cms, m_locale, m_field.getName(), true, true);
400        }
401
402        /**
403         * Gets the visibility configuration string for the field.
404         *
405         * @return the visibility configuration string
406         */
407        public String getVisibility() {
408
409            I_CmsXmlContentHandler handler = m_root.getContentDefinition().getContentHandler();
410            if (handler instanceof CmsDefaultXmlContentHandler) {
411                return ((CmsDefaultXmlContentHandler)handler).getVisibilityConfigString(m_path);
412            }
413            return null;
414        }
415
416        /**
417         * Gets the widget.
418         *
419         * @return the widget
420         */
421        @SuppressWarnings("synthetic-access")
422        public String getWidget() {
423
424            if (m_contentDefinition != null) {
425                return null;
426            }
427            WidgetInfo widgetInfo = getWidgetInfo();
428            if (widgetInfo != null) {
429                return widgetInfo.getWidget().getClass().getName();
430            } else {
431                LOG.error("Widget info not defined for " + m_path + " in " + m_root.getContentDefinition());
432                return null;
433            }
434        }
435
436        /**
437         * Gets the widget configuration.
438         *
439         * @return the widget configuration
440         */
441        @SuppressWarnings("synthetic-access")
442        public String getWidgetConfig() {
443
444            if (m_contentDefinition != null) {
445                return null;
446            }
447            WidgetInfo widgetInfo = getWidgetInfo();
448            if (widgetInfo != null) {
449                return widgetInfo.getWidget().getConfiguration();
450            } else {
451                LOG.error("Widget info not defined for " + m_path + " in " + m_root.getContentDefinition());
452                return null;
453            }
454
455        }
456
457        /**
458         * Gets the description or description key.
459         *
460         * @param resolveMode the resolve mode
461         * @param locale the locale to use
462         * @return the description or localization key
463         */
464        @SuppressWarnings("synthetic-access")
465        private String getDescription(ResolveMode resolveMode, Locale locale) {
466
467            if (m_field == null) {
468                return null;
469            }
470            StringBuffer result = new StringBuffer(64);
471            I_CmsXmlContentHandler handler = m_field.getContentDefinition().getContentHandler();
472            if (handler instanceof CmsDefaultXmlContentHandler) {
473                CmsDefaultXmlContentHandler defaultHandler = (CmsDefaultXmlContentHandler)handler;
474                String help = defaultHandler.getFieldHelp().get(m_field.getName());
475                if (help != null) {
476                    CmsMacroResolver resolver = new CmsMacroResolver();
477                    if (resolveMode == ResolveMode.raw) {
478                        return help;
479                    } else {
480                        if (resolveMode == ResolveMode.key) {
481                            resolver = new CmsKeyDummyMacroResolver(resolver);
482                        }
483                        resolver.setCmsObject(m_cms);
484                        resolver.setKeepEmptyMacros(true);
485                        resolver.setMessages(getMessages(locale));
486
487                        String val = resolver.resolveMacros(help);
488                        if (resolveMode == ResolveMode.key) {
489                            return CmsKeyDummyMacroResolver.getKey(val);
490                        } else {
491                            return val;
492                        }
493                    }
494                }
495            }
496            result.append(A_CmsWidget.LABEL_PREFIX);
497            result.append(getTypeKey(m_field));
498            result.append(A_CmsWidget.HELP_POSTFIX);
499            switch (resolveMode) {
500                case key:
501                case raw:
502                    return result.toString();
503                case text:
504                default:
505                    return getMessages(locale).keyDefault(result.toString(), null);
506            }
507        }
508
509        /**
510         * Gets the display name or localization key.
511         *
512         * @param keyOnly true if only the localization key should be returned rather than the localized display name
513         * @return the display name or localization key
514         */
515        @SuppressWarnings("synthetic-access")
516        private String getDisplayName(ResolveMode resolveMode) {
517
518            if (m_field == null) {
519                return null;
520            }
521            I_CmsXmlContentHandler handler = m_field.getContentDefinition().getContentHandler();
522            if (handler instanceof CmsDefaultXmlContentHandler) {
523                CmsDefaultXmlContentHandler defaultHandler = (CmsDefaultXmlContentHandler)handler;
524                String label = defaultHandler.getFieldLabels().get(m_field.getName());
525                if (label != null) {
526                    if (resolveMode == ResolveMode.raw) {
527                        return label;
528                    } else {
529                        CmsMacroResolver resolver = new CmsMacroResolver();
530                        if (resolveMode == ResolveMode.key) {
531                            resolver = new CmsKeyDummyMacroResolver(resolver);
532                        }
533                        resolver.setCmsObject(m_cms);
534                        resolver.setKeepEmptyMacros(true);
535                        resolver.setMessages(m_messages);
536                        String val = resolver.resolveMacros(label);
537                        if (resolveMode == ResolveMode.key) {
538                            return CmsKeyDummyMacroResolver.getKey(val);
539                        } else {
540                            return val;
541                        }
542                    }
543                }
544            }
545            StringBuffer result = new StringBuffer(64);
546            result.append(A_CmsWidget.LABEL_PREFIX);
547            result.append(getTypeKey(m_field));
548            switch (resolveMode) {
549                case raw:
550                case key:
551                    return result.toString();
552                case text:
553                default:
554                    return m_messages.keyDefault(result.toString(), m_field.getName());
555            }
556        }
557
558        /**
559         * Returns the schema type message key.<p>
560         *
561         * @param value the schema type
562         *
563         * @return the schema type message key
564         */
565        private String getTypeKey(I_CmsXmlSchemaType value) {
566
567            StringBuffer result = new StringBuffer(64);
568            result.append(value.getContentDefinition().getInnerName());
569            result.append('.');
570            result.append(value.getName());
571            return result.toString();
572        }
573
574        /**
575         * Gets the widget info.
576         *
577         * @return the widget info
578         */
579        @SuppressWarnings("synthetic-access")
580        private WidgetInfo getWidgetInfo() {
581
582            if (m_widgetInfo == null) {
583                m_widgetInfo = CmsWidgetUtil.collectWidgetInfo(
584                    m_cms,
585                    m_root.getContentDefinition(),
586                    m_path,
587                    null,
588                    m_cms.getRequestContext().getLocale());
589            }
590            return m_widgetInfo;
591
592        }
593
594        /**
595         * Process content definition.
596         *
597         * @param contentDef the content definition
598         */
599        private void processContentDefinition(CmsXmlContentDefinition contentDef) {
600
601            List<I_CmsXmlSchemaType> fields = contentDef.getTypeSequence();
602            for (I_CmsXmlSchemaType field : fields) {
603                String name = field.getName();
604                m_children.put(name, new Field(field, combinePaths(m_path, name)));
605            }
606        }
607
608    }
609
610    /**
611     * Represents the a single editor tab and its fields.
612     */
613    public class Tab implements I_CmsInfoWrapper {
614
615        private Locale m_defaultLocale;
616
617        /** The fields. */
618        private List<Field> m_fields = new ArrayList<>();
619
620        private int m_tabIndex = -1;
621
622        private Function<Locale, List<CmsTabInfo>> m_tabInfoProvider;
623
624        /**
625         * Default constructor - doesn't initialize anything.
626         *
627         * <p>Used for 'dummy' tab that is generated when no actual tabs are configured.
628         *
629         */
630        public Tab() {
631
632        }
633
634        /**
635         * Constructor for tab that is actually configured in the schema.
636         *
637         * @param defaultLocale the default locale
638         * @param tabIndex the current tab index
639         * @param tabInfoProvider the function that provides the list of localized tab info objects
640         */
641        public Tab(Locale defaultLocale, int tabIndex, Function<Locale, List<CmsTabInfo>> tabInfoProvider) {
642
643            m_tabInfoProvider = tabInfoProvider;
644            m_tabIndex = tabIndex;
645            m_defaultLocale = defaultLocale;
646        }
647
648        /**
649         * Adds a field.
650         *
651         * @param field the field to add
652         */
653        public void add(Field field) {
654
655            m_fields.add(field);
656        }
657
658        /**
659         * Gets the description.
660         *
661         * @return the description
662         */
663        public String getDescription() {
664
665            return localizedTabInfo(null, null, tabInfo -> tabInfo.getDescription());
666        }
667
668        /**
669         * Gets the localized description.
670         * 
671         * @param locale the locale 
672         * @return the description for the locale
673         */
674        @Override
675        public String getDescription(Locale locale) {
676
677            return localizedTabInfo(locale, null, tab -> tab.getDescription());
678        }
679
680        /**
681         * Gets the description key.
682         *
683         * @return the description key
684         */
685        public String getDescriptionKey() {
686
687            return localizedTabInfo(null, null, tabInfo -> tabInfo.getDescriptionKey());
688        }
689
690        /**
691         * Gets the raw description string.
692         *
693         * @return the raw description
694         */
695        public String getDescriptionRaw() {
696
697            return localizedTabInfo(null, null, tab -> tab.getDescriptionRaw());
698        }
699
700        /**
701         * Gets the display name.
702         *
703         * @return the display name
704         */
705        public String getDisplayName() {
706
707            return localizedTabInfo(null, null, tab -> tab.getTabName());
708        }
709
710        /**
711         * Gets the display name key.
712         *
713         * @return the display name key
714         */
715        public String getDisplayNameKey() {
716
717            return localizedTabInfo(null, null, tab -> tab.getTabNameKey());
718        }
719
720        /**
721         * Gets the raw display name string.
722         *
723         * @return the raw display name string
724         */
725        public String getDisplayNameRaw() {
726
727            return localizedTabInfo(null, null, tab -> tab.getTabNameRaw());
728
729        }
730
731        /**
732         * Gets the fields.
733         *
734         * @return the fields
735         */
736        public List<Field> getFields() {
737
738            return m_fields;
739        }
740
741        /**
742         * Helper method for reading localized tab information.
743         *
744         * @param locale the locale (null for current locale)
745         * @param defaultValue the default value
746         * @param function a function to extract the relevant information from a localized CmsTabInfo instance
747         *
748         * @return the result
749         */
750        private String localizedTabInfo(Locale locale, String defaultValue, Function<CmsTabInfo, String> function) {
751
752            if (locale == null) {
753                locale = m_defaultLocale;
754            }
755            if ((m_tabIndex != -1) && (m_tabInfoProvider != null)) {
756                CmsTabInfo tabInfo = m_tabInfoProvider.apply(locale).get(m_tabIndex);
757                return function.apply(tabInfo);
758            }
759            return defaultValue;
760        }
761
762    }
763
764    /** The logger instance for this class. */
765    private static final Log LOG = CmsLog.getLog(CmsSchemaInfo.class);
766
767    /** The CMS context. */
768    private CmsObject m_cms;
769
770    private CmsXmlContentDefinition m_contentDefinition;
771
772    /** The locale. */
773    private Locale m_locale;
774
775    /** The messages. */
776    private CmsMultiMessages m_messages;
777
778    /** Cache of message objects. */
779    private Map<Locale, CmsMultiMessages> m_messagesByLocale = new HashMap<>();
780
781    /** The root field instanec representing the whole schema. */
782    private Field m_root;
783
784    private Map<Locale, List<CmsTabInfo>> m_tabInfoCache = new HashMap<>();
785
786    /** The tabs. */
787    private List<Tab> m_tabs;
788
789    /**
790     * Creates a new instance.
791     *
792     * @param cms the CMS context
793     * @param contentDef the content definition
794     */
795    public CmsSchemaInfo(CmsObject cms, CmsXmlContentDefinition contentDef) {
796
797        m_cms = cms;
798        m_locale = cms.getRequestContext().getLocale();
799        m_root = new Field(contentDef);
800        m_contentDefinition = contentDef;
801
802        Locale locale = cms.getRequestContext().getLocale();
803        m_messages = getMessages(locale);
804        initTabs();
805
806    }
807
808    /**
809     * Combine schema paths.
810     *
811     * @param a the first path
812     * @param b the second path
813     * @return the combined paths
814     */
815    static String combinePaths(String a, String b) {
816
817        if ("".equals(a)) {
818            return b;
819        }
820        return a + "/" + b;
821    }
822
823    /**
824     * Gets the root node.
825     *
826     * @return the root node
827     */
828    public Field getRoot() {
829
830        return m_root;
831    }
832
833    /**
834     * Gets the tabs.
835     *
836     * @return the tabs
837     */
838    public List<Tab> getTabs() {
839
840        return m_tabs;
841    }
842
843    /**
844     * Checks for tabs.
845     *
846     * @return true, if there are tabs
847     */
848    public boolean hasTabs() {
849
850        return m_tabs.get(0).getDisplayName() == null;
851    }
852
853    /**
854     * Creates the message object for the given locale.
855     * 
856     * @param locale the locale 
857     * @return the messages for the given locale 
858     */
859    private CmsMultiMessages createMessages(Locale locale) {
860
861        CmsMessages messages;
862        CmsMultiMessages resultMessages = new CmsMultiMessages(locale);
863        resultMessages.setFallbackHandler(m_contentDefinition.getContentHandler().getMessageKeyHandler());
864        try {
865            messages = OpenCms.getWorkplaceManager().getMessages(locale);
866            if (messages != null) {
867                resultMessages.addMessages(messages);
868            }
869            messages = m_contentDefinition.getContentHandler().getMessages(locale);
870            if (messages != null) {
871                resultMessages.addMessages(messages);
872            }
873        } catch (Exception e) {
874            LOG.debug(e.getMessage(), e);
875        }
876        return resultMessages;
877    }
878
879    /**
880     * Gets or creates the messages object for the given locale.
881     *
882     * @param locale the locale to use
883     * @return the messages for the locale
884     */
885    private CmsMultiMessages getMessages(Locale locale) {
886
887        return m_messagesByLocale.computeIfAbsent(locale, l -> {
888            return createMessages(l);
889        });
890    }
891
892    /**
893     * Gets or creates the localized tab information for the given locale.
894     * 
895     * @param locale the locale 
896     * @return the localized tab information
897     */
898    private List<CmsTabInfo> getTabInfos(Locale locale) {
899
900        return m_tabInfoCache.computeIfAbsent(
901            locale,
902            l -> CmsContentTypeVisitor.collectTabInfos(m_cms, m_root.getContentDefinition(), getMessages(l)));
903    }
904
905    /**
906     * Initializes the tabs.
907     */
908    private void initTabs() {
909
910        List<CmsTabInfo> tabs = CmsContentTypeVisitor.collectTabInfos(m_cms, m_root.getContentDefinition(), m_messages);
911
912        List<Tab> result = new ArrayList<>();
913        if ((tabs == null) || (tabs.size() == 0)) {
914            Tab defaultTab = new Tab();
915            result.add(defaultTab);
916            defaultTab.getFields().addAll(m_root.getChildren());
917        } else {
918            int index = 0;
919            for (Field node : m_root.getChildren()) {
920                if ((index < tabs.size()) && node.getName().equals(tabs.get(index).getStartName())) {
921                    Tab tab = new Tab(m_cms.getRequestContext().getLocale(), index, locale -> getTabInfos(locale));
922                    result.add(tab);
923                    index += 1;
924                }
925                if (result.size() > 0) {
926                    result.get(result.size() - 1).add(node);
927                }
928            }
929        }
930        m_tabs = result;
931    }
932}