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 Integer.valueOf(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}