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