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