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 GmbH & Co. KG, 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;
029
030import org.opencms.file.CmsObject;
031import org.opencms.i18n.CmsEncoder;
032import org.opencms.main.CmsLog;
033import org.opencms.main.CmsRuntimeException;
034import org.opencms.main.OpenCms;
035import org.opencms.security.CmsRole;
036import org.opencms.security.CmsRoleViolationException;
037import org.opencms.util.CmsStringUtil;
038import org.opencms.widgets.I_CmsWidget;
039import org.opencms.xml.content.I_CmsXmlContentHandler;
040import org.opencms.xml.types.CmsXmlNestedContentDefinition;
041import org.opencms.xml.types.I_CmsXmlSchemaType;
042
043import java.io.UnsupportedEncodingException;
044import java.util.ArrayList;
045import java.util.Collections;
046import java.util.HashMap;
047import java.util.Iterator;
048import java.util.LinkedHashMap;
049import java.util.List;
050import java.util.Map;
051import java.util.Set;
052
053import org.apache.commons.collections.FastHashMap;
054import org.apache.commons.logging.Log;
055
056import org.dom4j.Document;
057import org.dom4j.Element;
058
059/**
060 * Manager class for registered OpenCms XML content types and content collectors.<p>
061 *
062 * @since 6.0.0
063 */
064public class CmsXmlContentTypeManager {
065
066    /** The log object for this class. */
067    private static final Log LOG = CmsLog.getLog(CmsXmlContentTypeManager.class);
068
069    /** Stores the initialized XML content handlers. */
070    private Map<String, I_CmsXmlContentHandler> m_contentHandlers;
071
072    /** Stores the registered content widgets. */
073    private Map<String, I_CmsWidget> m_defaultWidgets;
074
075    /** Stores the registered content types. */
076    private Map<String, I_CmsXmlSchemaType> m_registeredTypes;
077
078    /** Stores the registered content widgets by class name. */
079    private Map<String, I_CmsWidget> m_registeredWidgets;
080
081    /** The alias names for the widgets. */
082    private Map<String, String> m_widgetAliases;
083
084    /** The default configurations for the widgets. */
085    private Map<String, String> m_widgetDefaultConfigurations;
086
087    /**
088     * Creates a new content type manager.<p>
089     */
090    @SuppressWarnings("unchecked")
091    public CmsXmlContentTypeManager() {
092
093        // use the fast hash map implementation since there will be far more read then write accesses
094
095        m_registeredTypes = new HashMap<String, I_CmsXmlSchemaType>();
096        m_defaultWidgets = new HashMap<String, I_CmsWidget>();
097        m_registeredWidgets = new LinkedHashMap<String, I_CmsWidget>();
098        m_widgetAliases = new LinkedHashMap<String, String>();
099        m_widgetDefaultConfigurations = new HashMap<String, String>();
100
101        FastHashMap fastMap = new FastHashMap();
102        fastMap.setFast(true);
103        m_contentHandlers = fastMap;
104
105        if (CmsLog.INIT.isInfoEnabled()) {
106            CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_START_CONTENT_CONFIG_0));
107        }
108    }
109
110    /**
111     * Returns a statically initialized instance of an XML content type manager (for test cases only).<p>
112     *
113     * @return a statically initialized instance of an XML content type manager
114     */
115    public static CmsXmlContentTypeManager createTypeManagerForTestCases() {
116
117        CmsXmlContentTypeManager typeManager = new CmsXmlContentTypeManager();
118
119        typeManager.addWidget("org.opencms.widgets.CmsCalendarWidget", null, null);
120        typeManager.addWidget("org.opencms.widgets.CmsHtmlWidget", null, null);
121        typeManager.addWidget("org.opencms.widgets.CmsInputWidget", null, null);
122
123        typeManager.addSchemaType("org.opencms.xml.types.CmsXmlDateTimeValue", "org.opencms.widgets.CmsCalendarWidget");
124        typeManager.addSchemaType("org.opencms.xml.types.CmsXmlHtmlValue", "org.opencms.widgets.CmsHtmlWidget");
125        typeManager.addSchemaType("org.opencms.xml.types.CmsXmlLocaleValue", "org.opencms.widgets.CmsInputWidget");
126        typeManager.addSchemaType("org.opencms.xml.types.CmsXmlStringValue", "org.opencms.widgets.CmsInputWidget");
127        typeManager.addSchemaType(
128            "org.opencms.xml.types.CmsXmlPlainTextStringValue",
129            "org.opencms.widgets.CmsInputWidget");
130
131        try {
132            typeManager.initialize(null);
133        } catch (CmsRoleViolationException e) {
134            // this should never happen
135            throw new CmsRuntimeException(Messages.get().container(Messages.ERR_INIT_TYPE_MANAGER_0));
136        }
137        return typeManager;
138    }
139
140    /**
141     * Adds a XML content schema type class to the registered XML content types.<p>
142     *
143     * @param clazz the XML content schema type class to add
144     *
145     * @return the created instance of the XML content schema type
146     *
147     * @throws CmsXmlException in case the class is not an instance of {@link I_CmsXmlSchemaType}
148     */
149    public I_CmsXmlSchemaType addContentType(Class<?> clazz) throws CmsXmlException {
150
151        I_CmsXmlSchemaType type;
152        try {
153            type = (I_CmsXmlSchemaType)clazz.newInstance();
154        } catch (InstantiationException e) {
155            throw new CmsXmlException(Messages.get().container(Messages.ERR_INVALID_XCC_TYPE_REGISTERED_0));
156        } catch (IllegalAccessException e) {
157            throw new CmsXmlException(Messages.get().container(Messages.ERR_INVALID_XCC_TYPE_REGISTERED_0));
158        } catch (ClassCastException e) {
159            throw new CmsXmlException(Messages.get().container(Messages.ERR_INVALID_XCC_TYPE_REGISTERED_0));
160        }
161        m_registeredTypes.put(type.getTypeName(), type);
162        return type;
163    }
164
165    /**
166     * Adds a new XML content type schema class and XML widget to the manager by class names.<p>
167     *
168     * @param className class name of the XML content schema type class to add
169     * @param defaultWidget class name of the default XML widget class for the added XML content type
170     */
171    public void addSchemaType(String className, String defaultWidget) {
172
173        Class<?> classClazz;
174        // initialize class for schema type
175        try {
176            classClazz = Class.forName(className);
177        } catch (ClassNotFoundException e) {
178            LOG.error(
179                Messages.get().getBundle().key(Messages.LOG_XML_CONTENT_SCHEMA_TYPE_CLASS_NOT_FOUND_1, className),
180                e);
181            return;
182        }
183
184        // create the schema type and add it to the internal list
185        I_CmsXmlSchemaType type;
186        try {
187            type = addContentType(classClazz);
188        } catch (Exception e) {
189            LOG.error(
190                Messages.get().getBundle().key(
191                    Messages.LOG_INIT_XML_CONTENT_SCHEMA_TYPE_CLASS_ERROR_1,
192                    classClazz.getName()),
193                e);
194            return;
195        }
196
197        // add the editor widget for the schema type
198        I_CmsWidget widget = getWidget(defaultWidget);
199        if (widget == null) {
200            LOG.error(
201                Messages.get().getBundle().key(
202                    Messages.LOG_INIT_DEFAULT_WIDGET_FOR_CONTENT_TYPE_2,
203                    defaultWidget,
204                    type.getTypeName()));
205            return;
206        }
207
208        // store the registered default widget
209        m_defaultWidgets.put(type.getTypeName(), widget);
210
211        if (CmsLog.INIT.isInfoEnabled()) {
212            CmsLog.INIT.info(
213                Messages.get().getBundle().key(
214                    Messages.INIT_ADD_ST_USING_WIDGET_2,
215                    type.getTypeName(),
216                    widget.getClass().getName()));
217        }
218    }
219
220    /**
221     * Adds a XML content editor widget class, making this widget available for the XML content editor.<p>
222     *
223     * @param className the widget class to add
224     * @param aliases the (optional) alias names to use for the widget class
225     * @param defaultConfiguration the default configuration of the widget
226     */
227    public void addWidget(String className, List<String> aliases, String defaultConfiguration) {
228
229        Class<?> widgetClazz;
230        I_CmsWidget widget;
231        if (aliases == null) {
232            aliases = Collections.emptyList();
233        }
234        try {
235            widgetClazz = Class.forName(className);
236            widget = (I_CmsWidget)widgetClazz.newInstance();
237        } catch (Exception e) {
238            LOG.error(Messages.get().getBundle().key(Messages.LOG_XML_WIDGET_INITIALIZING_ERROR_1, className), e);
239            return;
240        }
241
242        m_registeredWidgets.put(widgetClazz.getName(), widget);
243
244        for (String alias : aliases) {
245            String prev = m_widgetAliases.get(alias);
246            if (prev != null) {
247                LOG.warn("Duplicate widget alias " + alias + " for " + prev + ", " + widgetClazz.getName());
248            }
249            m_widgetAliases.put(alias, widgetClazz.getName());
250        }
251
252        if (CmsStringUtil.isNotEmpty(defaultConfiguration)) {
253            // put the default configuration to the lookup Map
254            m_widgetDefaultConfigurations.put(className, defaultConfiguration);
255        }
256
257        if (CmsLog.INIT.isInfoEnabled()) {
258            if (CmsStringUtil.isEmpty(defaultConfiguration)) {
259                CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_ADD_WIDGET_1, widgetClazz.getName()));
260            } else {
261                CmsLog.INIT.info(
262                    Messages.get().getBundle().key(
263                        Messages.INIT_ADD_WIDGET_CONFIG_2,
264                        widgetClazz.getName(),
265                        defaultConfiguration));
266            }
267        }
268    }
269
270    /**
271     * Returns the XML content handler instance class for the specified class name.<p>
272     *
273     * Only one instance of an XML content handler class per content definition name will be generated,
274     * and that instance will be cached and re-used for all operations.<p>
275     *
276     * @param className the name of the XML content handler to return
277     * @param schemaLocation the schema location of the XML content definition that handler belongs to
278     *
279     * @return the XML content handler class
280     *
281     * @throws CmsXmlException if something goes wrong
282     */
283    public I_CmsXmlContentHandler getContentHandler(String className, String schemaLocation) throws CmsXmlException {
284
285        // create a unique key for the content deinition / class name combo
286        StringBuffer buffer = new StringBuffer(128);
287        buffer.append(schemaLocation);
288        buffer.append('#');
289        buffer.append(className);
290        String key = buffer.toString();
291
292        // look up the content handler from the cache
293        I_CmsXmlContentHandler contentHandler = m_contentHandlers.get(key);
294        if (contentHandler != null) {
295            return contentHandler;
296        }
297
298        // generate an instance for the content handler
299        try {
300            contentHandler = (I_CmsXmlContentHandler)Class.forName(className).newInstance();
301        } catch (InstantiationException e) {
302            throw new CmsXmlException(Messages.get().container(Messages.ERR_INVALID_CONTENT_HANDLER_1, key));
303        } catch (IllegalAccessException e) {
304            throw new CmsXmlException(Messages.get().container(Messages.ERR_INVALID_CONTENT_HANDLER_1, key));
305        } catch (ClassCastException e) {
306            throw new CmsXmlException(Messages.get().container(Messages.ERR_INVALID_CONTENT_HANDLER_1, key));
307        } catch (ClassNotFoundException e) {
308            throw new CmsXmlException(Messages.get().container(Messages.ERR_INVALID_CONTENT_HANDLER_1, key));
309        }
310
311        // cache and return the content handler instance
312        m_contentHandlers.put(key, contentHandler);
313        return contentHandler;
314    }
315
316    /**
317     * Generates an initialized instance of a XML content type definition
318     * from the given XML schema element.<p>
319     *
320     * @param typeElement the element to generate the XML content type definition from
321     * @param nestedDefinitions the nested (included) XML content sub-definitions
322     *
323     * @return an initialized instance of a XML content type definition
324     * @throws CmsXmlException in case the element does not describe a valid XML content type definition
325     */
326    public I_CmsXmlSchemaType getContentType(Element typeElement, Set<CmsXmlContentDefinition> nestedDefinitions)
327    throws CmsXmlException {
328
329        if (!CmsXmlContentDefinition.XSD_NODE_ELEMENT.equals(typeElement.getQName())) {
330            throw new CmsXmlException(Messages.get().container(Messages.ERR_INVALID_CD_SCHEMA_STRUCTURE_0));
331        }
332        if (typeElement.elements().size() > 0) {
333            throw new CmsXmlException(Messages.get().container(Messages.ERR_INVALID_CD_SCHEMA_STRUCTURE_0));
334        }
335
336        String elementName = typeElement.attributeValue(CmsXmlContentDefinition.XSD_ATTRIBUTE_NAME);
337        String typeName = typeElement.attributeValue(CmsXmlContentDefinition.XSD_ATTRIBUTE_TYPE);
338        String defaultValue = typeElement.attributeValue(CmsXmlContentDefinition.XSD_ATTRIBUTE_DEFAULT);
339        String maxOccrs = typeElement.attributeValue(CmsXmlContentDefinition.XSD_ATTRIBUTE_MAX_OCCURS);
340        String minOccrs = typeElement.attributeValue(CmsXmlContentDefinition.XSD_ATTRIBUTE_MIN_OCCURS);
341
342        if (CmsStringUtil.isEmpty(elementName)) {
343            throw new CmsXmlException(
344                Messages.get().container(
345                    Messages.ERR_EL_MISSING_ATTRIBUTE_2,
346                    typeElement.getName(),
347                    CmsXmlContentDefinition.XSD_ATTRIBUTE_NAME));
348        }
349        if (CmsStringUtil.isEmpty(typeName)) {
350            throw new CmsXmlException(
351                Messages.get().container(
352                    Messages.ERR_EL_MISSING_ATTRIBUTE_2,
353                    typeElement.getName(),
354                    CmsXmlContentDefinition.XSD_ATTRIBUTE_TYPE));
355        }
356
357        boolean simpleType = true;
358        I_CmsXmlSchemaType schemaType = m_registeredTypes.get(typeName);
359        if (schemaType == null) {
360
361            // the name is not a simple type, try to resolve from the nested schemas
362            Iterator<CmsXmlContentDefinition> i = nestedDefinitions.iterator();
363            while (i.hasNext()) {
364
365                CmsXmlContentDefinition cd = i.next();
366                if (typeName.equals(cd.getTypeName())) {
367
368                    simpleType = false;
369                    return new CmsXmlNestedContentDefinition(cd, elementName, minOccrs, maxOccrs);
370                }
371            }
372
373            if (simpleType) {
374                throw new CmsXmlException(Messages.get().container(Messages.ERR_UNKNOWN_SCHEMA_1, typeName));
375            }
376        }
377
378        if (simpleType && (schemaType != null)) {
379            schemaType = schemaType.newInstance(elementName, minOccrs, maxOccrs);
380
381            if (CmsStringUtil.isNotEmpty(defaultValue)) {
382                schemaType.setDefault(defaultValue);
383            }
384        }
385
386        return schemaType;
387    }
388
389    /**
390     * Returns the content type registered with the given name, or <code>null</code>.<p>
391     *
392     * @param typeName the name to look up the content type for
393     * @return the content type registered with the given name, or <code>null</code>
394     */
395    public I_CmsXmlSchemaType getContentType(String typeName) {
396
397        return m_registeredTypes.get(typeName);
398    }
399
400    /**
401     * Returns a fresh XML content handler instance for the specified class name.<p>
402     *
403     * @param className the name of the XML content handler to return
404     *
405     * @return the XML content handler class
406     *
407     * @throws CmsXmlException if something goes wrong
408     */
409    public I_CmsXmlContentHandler getFreshContentHandler(String className) throws CmsXmlException {
410
411        I_CmsXmlContentHandler contentHandler;
412        // generate an instance for the content handler
413        try {
414            contentHandler = (I_CmsXmlContentHandler)Class.forName(className).newInstance();
415        } catch (InstantiationException e) {
416            throw new CmsXmlException(Messages.get().container(Messages.ERR_INVALID_CONTENT_HANDLER_1, className));
417        } catch (IllegalAccessException e) {
418            throw new CmsXmlException(Messages.get().container(Messages.ERR_INVALID_CONTENT_HANDLER_1, className));
419        } catch (ClassCastException e) {
420            throw new CmsXmlException(Messages.get().container(Messages.ERR_INVALID_CONTENT_HANDLER_1, className));
421        } catch (ClassNotFoundException e) {
422            throw new CmsXmlException(Messages.get().container(Messages.ERR_INVALID_CONTENT_HANDLER_1, className));
423        }
424        return contentHandler;
425    }
426
427    /**
428     * Returns an alphabetically sorted list of all configured XML content schema types.<p>
429     *
430     * @return an alphabetically sorted list of all configured XML content schema types
431     */
432    public List<I_CmsXmlSchemaType> getRegisteredSchemaTypes() {
433
434        List<I_CmsXmlSchemaType> result = new ArrayList<I_CmsXmlSchemaType>(m_registeredTypes.values());
435        Collections.sort(result);
436        return result;
437    }
438
439    /**
440     * Returns the alias for the given Widget class name, may be <code>null</code> if no alias is defined for
441     * the class.<p>
442     *
443     * @param className the name of the widget
444     * @return the alias for the given Widget class name, may be <code>null</code> if no alias is defined for
445     * the class
446     */
447    public List<String> getRegisteredWidgetAliases(String className) {
448
449        List<String> result = new ArrayList<>();
450        Iterator<Map.Entry<String, String>> i = m_widgetAliases.entrySet().iterator();
451        while (i.hasNext()) {
452            // key is alias name, value is class name
453            Map.Entry<String, String> e = i.next();
454            if (e.getValue().equals(className)) {
455                result.add(e.getKey());
456            }
457        }
458        return result;
459    }
460
461    /**
462     * Returns an alphabetically sorted list of the class names of all configured XML widgets.<p>
463     *
464     * @return an alphabetically sorted list of the class names of all configured XML widgets
465     */
466    public List<String> getRegisteredWidgetNames() {
467
468        List<String> result = new ArrayList<String>(m_registeredWidgets.keySet());
469        return result;
470    }
471
472    /**
473     * Returns an initialized widget class by it's class name or by it's alias.<p>
474     *
475     * @param name the class name or alias name to get the widget for
476     * @return the widget instance for the class name
477     */
478    public I_CmsWidget getWidget(String name) {
479
480        // first look up by class name
481        I_CmsWidget result = m_registeredWidgets.get(name);
482        if (result == null) {
483            // not found by class name, look up an alias
484            String className = m_widgetAliases.get(name);
485            if (className != null) {
486                result = m_registeredWidgets.get(className);
487            }
488        }
489        if (result != null) {
490            result = result.newInstance();
491        }
492        return result;
493    }
494
495    /**
496     * Returns the editor widget for the specified XML content type.<p>
497     *
498     * This will always return a fresh instance if it doesn't return null.
499     *
500     * @param typeName the name of the XML content type to get the widget for
501     * @return the editor widget for the specified XML content type
502     */
503    public I_CmsWidget getWidgetDefault(String typeName) {
504
505        I_CmsWidget result = m_defaultWidgets.get(typeName);
506        if (result != null) {
507            result = result.newInstance();
508        }
509        return result;
510    }
511
512    /**
513     * Returns the default widget configuration set in <code>opencms-vfs.xml</code> or <code>null</code> if nothing is found.<p>
514     *
515     * @param widget the widget instance to get the default configuration for
516     *
517     * @return the default widget configuration
518     */
519    public String getWidgetDefaultConfiguration(I_CmsWidget widget) {
520
521        return m_widgetDefaultConfigurations.get(widget.getClass().getName());
522    }
523
524    /**
525     * Returns the default widget configuration set in <code>opencms-vfs.xml</code> or <code>null</code> if nothing is found.<p>
526     *
527     * @param name the class name or alias name to get the default configuration for
528     *
529     * @return the default widget configuration
530     */
531    public String getWidgetDefaultConfiguration(String name) {
532
533        if (m_registeredWidgets.containsKey(name)) {
534            return m_widgetDefaultConfigurations.get(name);
535        }
536        // not found by class name, look up an alias
537        String className = m_widgetAliases.get(name);
538        if (className != null) {
539            return m_widgetDefaultConfigurations.get(className);
540        }
541        return null;
542    }
543
544    /**
545     * Initializes XML content types managed in this XML content type manager.<p>
546     *
547     * @param cms an initialized OpenCms user context with "Administrator" role permissions
548     *
549     * @throws CmsRoleViolationException in case the provided OpenCms user context doea not have "Administrator" role permissions
550     */
551    public synchronized void initialize(CmsObject cms) throws CmsRoleViolationException {
552
553        if (OpenCms.getRunLevel() > OpenCms.RUNLEVEL_1_CORE_OBJECT) {
554
555            // simple test cases don't require this check
556            OpenCms.getRoleManager().checkRole(cms, CmsRole.ROOT_ADMIN);
557        }
558
559        // initialize the special entity resolver
560        CmsXmlEntityResolver.initialize(cms, getSchemaBytes());
561
562        if (CmsLog.INIT.isInfoEnabled()) {
563            CmsLog.INIT.info(
564                Messages.get().getBundle().key(
565                    Messages.INIT_NUM_ST_INITIALIZED_1,
566                    new Integer(m_registeredTypes.size())));
567        }
568    }
569
570    /**
571     * Returns a byte array to be used as input source for the configured XML content types.<p>
572     *
573     * @return a byte array to be used as input source for the configured XML content types
574     */
575    private byte[] getSchemaBytes() {
576
577        StringBuffer schema = new StringBuffer(512);
578        schema.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
579        schema.append("<xsd:schema xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" elementFormDefault=\"qualified\">");
580        Iterator<I_CmsXmlSchemaType> i = m_registeredTypes.values().iterator();
581        while (i.hasNext()) {
582            I_CmsXmlSchemaType type = i.next();
583            schema.append(type.getSchemaDefinition());
584        }
585        schema.append("</xsd:schema>");
586        String schemaStr = schema.toString();
587
588        try {
589            // pretty print the XML schema
590            // this helps in debugging the auto-generated schema includes
591            // since it makes them more human-readable
592            Document doc = CmsXmlUtils.unmarshalHelper(schemaStr, null);
593            schemaStr = CmsXmlUtils.marshal(doc, CmsEncoder.ENCODING_UTF_8);
594        } catch (CmsXmlException e) {
595            // should not ever happen
596            LOG.error(Messages.get().getBundle().key(Messages.LOG_PRETTY_PRINT_SCHEMA_BYTES_ERROR_0), e);
597        }
598        if (LOG.isInfoEnabled()) {
599            LOG.info(
600                Messages.get().getBundle().key(
601                    Messages.LOG_XML_TYPE_DEFINITION_XSD_2,
602                    CmsXmlContentDefinition.XSD_INCLUDE_OPENCMS,
603                    schemaStr));
604        }
605        try {
606            return schemaStr.getBytes(CmsEncoder.ENCODING_UTF_8);
607        } catch (UnsupportedEncodingException e) {
608            // should not happen since the default encoding of UTF-8 is always valid
609            LOG.error(Messages.get().getBundle().key(Messages.LOG_CONVERTING_SCHEMA_BYTES_ERROR_0), e);
610        }
611        return null;
612    }
613}