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.types; 029 030import org.opencms.file.CmsObject; 031import org.opencms.i18n.CmsEncoder; 032import org.opencms.main.CmsLog; 033import org.opencms.main.CmsRuntimeException; 034import org.opencms.relations.CmsRelationType; 035import org.opencms.util.CmsFileUtil; 036import org.opencms.util.CmsStringUtil; 037import org.opencms.widgets.I_CmsWidgetParameter; 038import org.opencms.xml.CmsXmlContentDefinition; 039import org.opencms.xml.CmsXmlGenericWrapper; 040import org.opencms.xml.CmsXmlUtils; 041import org.opencms.xml.I_CmsXmlDocument; 042 043import java.util.List; 044import java.util.Locale; 045 046import org.apache.commons.logging.Log; 047 048import org.dom4j.Element; 049 050/** 051 * Base class for XML content value implementations.<p> 052 * 053 * @since 6.0.0 054 */ 055public abstract class A_CmsXmlContentValue implements I_CmsXmlContentValue, I_CmsWidgetParameter { 056 057 /** The log object for this class. */ 058 private static final Log LOG = CmsLog.getLog(A_CmsXmlContentValue.class); 059 060 /** The default value for nodes of this value. */ 061 protected String m_defaultValue; 062 063 /** The XML content instance this value belongs to. */ 064 protected I_CmsXmlDocument m_document; 065 066 /** The XML element node that contains this value. */ 067 protected Element m_element; 068 069 /** The locale this value was generated for. */ 070 protected Locale m_locale; 071 072 /** The maximum occurrences of this value according to the parent schema. */ 073 protected int m_maxOccurs; 074 075 /** The minimum occurrences of this value according to the parent schema. */ 076 protected int m_minOccurs; 077 078 /** The configured XML node name of this value. */ 079 protected String m_name; 080 081 /** The content definition this schema type belongs to. */ 082 private CmsXmlContentDefinition m_contentDefinition; 083 084 /** The index position of this content value, with special handling for choice groups. */ 085 private int m_index; 086 087 /** The maximum index of this type that currently exist in the source document. */ 088 private int m_maxIndex; 089 090 /** Optional localized key prefix identifier. */ 091 private String m_prefix; 092 093 /** The index position of this content value in the XML order. */ 094 private int m_xmlIndex; 095 096 /** 097 * Default constructor for a XML content type 098 * that initializes some internal values.<p> 099 */ 100 protected A_CmsXmlContentValue() { 101 102 m_minOccurs = 0; 103 m_maxOccurs = Integer.MAX_VALUE; 104 m_index = -1; 105 m_xmlIndex = -1; 106 m_maxIndex = -1; 107 } 108 109 /** 110 * Initializes the required members for this XML content value.<p> 111 * 112 * @param document the XML content instance this value belongs to 113 * @param element the XML element that contains this value 114 * @param locale the locale this value is created for 115 * @param type the type instance to create the value for 116 */ 117 protected A_CmsXmlContentValue(I_CmsXmlDocument document, Element element, Locale locale, I_CmsXmlSchemaType type) { 118 119 m_element = element; 120 m_name = element.getName(); 121 m_document = document; 122 m_locale = locale; 123 m_minOccurs = type.getMinOccurs(); 124 m_maxOccurs = type.getMaxOccurs(); 125 m_contentDefinition = type.getContentDefinition(); 126 m_index = -1; 127 m_xmlIndex = -1; 128 m_maxIndex = -1; 129 } 130 131 /** 132 * Initializes the schema type descriptor values for this type descriptor.<p> 133 * 134 * @param name the name of the XML node containing the value according to the XML schema 135 * @param minOccurs minimum number of occurrences of this type according to the XML schema 136 * @param maxOccurs maximum number of occurrences of this type according to the XML schema 137 */ 138 protected A_CmsXmlContentValue(String name, String minOccurs, String maxOccurs) { 139 140 m_name = name; 141 m_minOccurs = 1; 142 if (CmsStringUtil.isNotEmpty(minOccurs)) { 143 try { 144 m_minOccurs = Integer.parseInt(minOccurs); 145 } catch (NumberFormatException e) { 146 // ignore 147 } 148 } 149 m_maxOccurs = 1; 150 if (CmsStringUtil.isNotEmpty(maxOccurs)) { 151 if (CmsXmlContentDefinition.XSD_ATTRIBUTE_VALUE_UNBOUNDED.equals(maxOccurs)) { 152 m_maxOccurs = Integer.MAX_VALUE; 153 } else { 154 try { 155 m_maxOccurs = Integer.parseInt(maxOccurs); 156 } catch (NumberFormatException e) { 157 // ignore 158 } 159 } 160 } 161 m_index = -1; 162 m_xmlIndex = -1; 163 m_maxIndex = -1; 164 } 165 166 /** 167 * Appends an element XML representation of this type to the given root node.<p> 168 * 169 * @param root the element to append the XML to 170 */ 171 public void appendXmlSchema(Element root) { 172 173 Element element = root.addElement(CmsXmlContentDefinition.XSD_NODE_ELEMENT); 174 element.addAttribute(CmsXmlContentDefinition.XSD_ATTRIBUTE_NAME, getName()); 175 element.addAttribute(CmsXmlContentDefinition.XSD_ATTRIBUTE_TYPE, getTypeName()); 176 if ((getMinOccurs() > 1) || (getMinOccurs() == 0)) { 177 element.addAttribute(CmsXmlContentDefinition.XSD_ATTRIBUTE_MIN_OCCURS, String.valueOf(getMinOccurs())); 178 } 179 if (getMaxOccurs() > 1) { 180 if (getMaxOccurs() == Integer.MAX_VALUE) { 181 element.addAttribute( 182 CmsXmlContentDefinition.XSD_ATTRIBUTE_MAX_OCCURS, 183 CmsXmlContentDefinition.XSD_ATTRIBUTE_VALUE_UNBOUNDED); 184 } else { 185 element.addAttribute(CmsXmlContentDefinition.XSD_ATTRIBUTE_MAX_OCCURS, String.valueOf(getMaxOccurs())); 186 } 187 } 188 } 189 190 /** 191 * @see java.lang.Comparable#compareTo(java.lang.Object) 192 */ 193 public int compareTo(I_CmsXmlSchemaType obj) { 194 195 if (obj == this) { 196 return 0; 197 } 198 return getTypeName().compareTo(obj.getTypeName()); 199 } 200 201 /** 202 * @see java.lang.Object#equals(java.lang.Object) 203 */ 204 @Override 205 public boolean equals(Object obj) { 206 207 if (obj == this) { 208 return true; 209 } 210 if (obj instanceof I_CmsXmlSchemaType) { 211 I_CmsXmlSchemaType other = (I_CmsXmlSchemaType)obj; 212 return (getName().equals(other.getName()) 213 && getTypeName().equals(other.getTypeName()) 214 && (getMinOccurs() == other.getMinOccurs()) 215 && (getMaxOccurs() == other.getMaxOccurs())); 216 } 217 return false; 218 } 219 220 /** 221 * @see org.opencms.xml.types.I_CmsXmlSchemaType#generateXml(org.opencms.file.CmsObject, org.opencms.xml.I_CmsXmlDocument, org.dom4j.Element, java.util.Locale) 222 */ 223 public Element generateXml(CmsObject cms, I_CmsXmlDocument document, Element root, Locale locale) { 224 225 Element element = root.addElement(getName()); 226 // get the default value from the content handler 227 String defaultValue = document.getHandler().getDefault(cms, this, locale); 228 if (defaultValue != null) { 229 try { 230 I_CmsXmlContentValue value = createValue(document, element, locale); 231 value.setStringValue(cms, defaultValue); 232 } catch (CmsRuntimeException e) { 233 // should not happen if default value is correct 234 LOG.error( 235 Messages.get().getBundle().key(Messages.ERR_XMLCONTENT_INVALID_ELEM_DEFAULT_1, defaultValue), 236 e); 237 element.clearContent(); 238 } 239 } 240 return element; 241 } 242 243 /** 244 * @see org.opencms.xml.types.I_CmsXmlSchemaType#getChoiceMaxOccurs() 245 */ 246 public int getChoiceMaxOccurs() { 247 248 return 0; 249 } 250 251 /** 252 * @see org.opencms.xml.types.I_CmsXmlSchemaType#getContentDefinition() 253 */ 254 public CmsXmlContentDefinition getContentDefinition() { 255 256 return m_contentDefinition; 257 } 258 259 /** 260 * @see org.opencms.widgets.I_CmsWidgetParameter#getDefault(org.opencms.file.CmsObject) 261 */ 262 public String getDefault(CmsObject cms) { 263 264 return m_contentDefinition.getContentHandler().getDefault(cms, this, getLocale()); 265 } 266 267 /** 268 * @see org.opencms.xml.types.I_CmsXmlSchemaType#getDefault(java.util.Locale) 269 */ 270 public String getDefault(Locale locale) { 271 272 return m_defaultValue; 273 } 274 275 /** 276 * @see org.opencms.xml.types.I_CmsXmlContentValue#getDocument() 277 */ 278 public I_CmsXmlDocument getDocument() { 279 280 return m_document; 281 } 282 283 /** 284 * @see org.opencms.xml.types.I_CmsXmlContentValue#getElement() 285 */ 286 public Element getElement() { 287 288 return m_element; 289 } 290 291 /** 292 * @see org.opencms.widgets.I_CmsWidgetParameter#getId() 293 */ 294 public String getId() { 295 296 if (m_element == null) { 297 return null; 298 } 299 300 StringBuffer result = new StringBuffer(128); 301 result.append(getTypeName()); 302 result.append('.'); 303 // the '[', ']' and '/' chars from the xpath are invalid for html id's 304 result.append(getPath().replace('[', '_').replace(']', '_').replace('/', '.')); 305 result.append('.'); 306 result.append(getIndex()); 307 return result.toString(); 308 } 309 310 /** 311 * @see org.opencms.xml.types.I_CmsXmlContentValue#getIndex() 312 */ 313 public int getIndex() { 314 315 if (m_index < 0) { 316 if (isChoiceOption()) { 317 m_index = m_element.getParent().elements().indexOf(m_element); 318 } else { 319 m_index = getXmlIndex(); 320 } 321 } 322 return m_index; 323 } 324 325 /** 326 * @see org.opencms.widgets.I_CmsWidgetParameter#getKey() 327 */ 328 public String getKey() { 329 330 StringBuffer result = new StringBuffer(128); 331 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(m_prefix)) { 332 result.append(m_prefix); 333 result.append('.'); 334 } 335 result.append(m_contentDefinition.getInnerName()); 336 result.append('.'); 337 result.append(getName()); 338 return result.toString(); 339 } 340 341 /** 342 * @see org.opencms.xml.types.I_CmsXmlContentValue#getLocale() 343 */ 344 public Locale getLocale() { 345 346 return m_locale; 347 } 348 349 /** 350 * @see org.opencms.xml.types.I_CmsXmlContentValue#getMaxIndex() 351 */ 352 public int getMaxIndex() { 353 354 if (m_maxIndex < 0) { 355 if (isChoiceOption()) { 356 m_maxIndex = m_element.getParent().elements().size(); 357 } else { 358 m_maxIndex = m_element.getParent().elements(m_element.getQName()).size(); 359 } 360 } 361 return m_maxIndex; 362 } 363 364 /** 365 * Returns the maximum occurrences of this type.<p> 366 * 367 * @return the maximum occurrences of this type 368 */ 369 public int getMaxOccurs() { 370 371 return m_maxOccurs; 372 } 373 374 /** 375 * Returns the minimum occurrences of this type.<p> 376 * 377 * @return the minimum occurrences of this type 378 */ 379 public int getMinOccurs() { 380 381 return m_minOccurs; 382 } 383 384 /** 385 * Returns the name.<p> 386 * 387 * @return the name 388 */ 389 public String getName() { 390 391 return m_name; 392 } 393 394 /** 395 * @see org.opencms.xml.types.I_CmsXmlContentValue#getPath() 396 */ 397 public String getPath() { 398 399 if (m_element == null) { 400 return ""; 401 } 402 String path = m_element.getUniquePath(); 403 // must remove the first 2 nodes because these are not required for XML content values 404 int pos = path.indexOf('/', path.indexOf('/', 1) + 1) + 1; 405 path = path.substring(pos); 406 407 // ensure all path elements have an index, even though this may not be required 408 return CmsXmlUtils.createXpath(path, 1); 409 } 410 411 /** 412 * @see org.opencms.xml.types.I_CmsXmlContentValue#getPlainText(org.opencms.file.CmsObject) 413 */ 414 public String getPlainText(CmsObject cms) { 415 416 return null; 417 } 418 419 /** 420 * @see org.opencms.xml.types.I_CmsXmlContentValue#getXmlIndex() 421 */ 422 public int getXmlIndex() { 423 424 if (m_xmlIndex < 0) { 425 m_xmlIndex = m_element.getParent().elements(m_element.getQName()).indexOf(m_element); 426 } 427 return m_xmlIndex; 428 } 429 430 /** 431 * @see org.opencms.widgets.I_CmsWidgetParameter#hasError() 432 */ 433 public boolean hasError() { 434 435 return false; 436 } 437 438 /** 439 * @see java.lang.Object#hashCode() 440 */ 441 @Override 442 public int hashCode() { 443 444 return getTypeName().hashCode(); 445 } 446 447 /** 448 * @see org.opencms.xml.types.I_CmsXmlSchemaType#isChoiceOption() 449 */ 450 public boolean isChoiceOption() { 451 452 return m_contentDefinition.getChoiceMaxOccurs() > 0; 453 } 454 455 /** 456 * @see org.opencms.xml.types.I_CmsXmlSchemaType#isChoiceType() 457 */ 458 public boolean isChoiceType() { 459 460 // this is overridden in the nested content definition 461 return false; 462 } 463 464 /** 465 * The default implementation always returns <code>true</code>.<p> 466 * 467 * @see org.opencms.xml.types.I_CmsXmlContentValue#isSearchable() 468 */ 469 public boolean isSearchable() { 470 471 return true; 472 } 473 474 /** 475 * @see org.opencms.xml.types.I_CmsXmlSchemaType#isSimpleType() 476 */ 477 public boolean isSimpleType() { 478 479 // the abstract base type should be used for simple types only 480 return true; 481 } 482 483 /** 484 * @see org.opencms.xml.types.I_CmsXmlContentValue#moveDown() 485 */ 486 public void moveDown() { 487 488 if (getIndex() > 0) { 489 // only move down if this element is not already at the first index position 490 moveValue(-1); 491 getDocument().initDocument(); 492 } 493 } 494 495 /** 496 * @see org.opencms.xml.types.I_CmsXmlContentValue#moveUp() 497 */ 498 public void moveUp() { 499 500 if (getIndex() < (getMaxIndex() - 1)) { 501 // only move up if this element is not already at the last index position 502 moveValue(1); 503 getDocument().initDocument(); 504 } 505 } 506 507 /** 508 * @see org.opencms.xml.types.I_CmsXmlSchemaType#setContentDefinition(org.opencms.xml.CmsXmlContentDefinition) 509 */ 510 public void setContentDefinition(CmsXmlContentDefinition contentDefinition) { 511 512 m_contentDefinition = contentDefinition; 513 } 514 515 /** 516 * Sets the default value for a node of this type.<p> 517 * 518 * @param defaultValue the default value to set 519 */ 520 public void setDefault(String defaultValue) { 521 522 m_defaultValue = defaultValue; 523 } 524 525 /** 526 * @see org.opencms.widgets.I_CmsWidgetParameter#setKeyPrefix(java.lang.String) 527 */ 528 public void setKeyPrefix(String prefix) { 529 530 m_prefix = prefix; 531 } 532 533 /** 534 * @see java.lang.Object#toString() 535 */ 536 @Override 537 public String toString() { 538 539 StringBuffer result = new StringBuffer(128); 540 result.append(getClass().getName()); 541 result.append(": name="); 542 result.append(getName()); 543 result.append(", type="); 544 result.append(getTypeName()); 545 result.append(", path="); 546 result.append(m_element == null ? null : getPath()); 547 String value; 548 try { 549 value = "'" + getStringValue(null) + "'"; 550 } catch (Exception e) { 551 value = "(CmsObject required to generate)"; 552 } 553 result.append(", value="); 554 result.append(value); 555 return result.toString(); 556 } 557 558 /** 559 * @see org.opencms.xml.types.I_CmsXmlSchemaType#validateValue(java.lang.String) 560 */ 561 public boolean validateValue(String value) { 562 563 return true; 564 } 565 566 /** 567 * Returns the relation type for the given path.<p> 568 * 569 * @param path the element path 570 * 571 * @return the relation type 572 */ 573 protected CmsRelationType getRelationType(String path) { 574 575 CmsRelationType result = getContentDefinition().getContentHandler().getRelationType(path); 576 I_CmsXmlDocument document = getDocument(); 577 if (document != null) { 578 // the relations set in the main content definition override relations set in the nested definition 579 result = document.getContentDefinition().getContentHandler().getRelationType(getPath(), result); 580 } else { 581 LOG.warn("Missing document while evaluating relation type for " + path); 582 } 583 return result; 584 } 585 586 /** 587 * Moves this XML content element up or down in the XML document.<p> 588 * 589 * Please note: No check is performed if the move violates the XML document schema!<p> 590 * 591 * @param step move the element up or down according to the given step value 592 */ 593 protected void moveValue(int step) { 594 595 Element e = getElement(); 596 Element parent = e.getParent(); 597 List<Element> siblings = CmsXmlGenericWrapper.elements(parent); 598 int idx = siblings.indexOf(e); 599 int newIdx = idx + step; 600 siblings.remove(idx); 601 siblings.add(newIdx, e); 602 m_index += step; 603 } 604 605 /** 606 * Convenience method to loads the XML schema definition for this value type from an external file.<p> 607 * 608 * @param schemaUri the schema uri to load the XML schema file from 609 * 610 * @return the loaded XML schema 611 * 612 * @throws CmsRuntimeException if something goes wrong 613 */ 614 protected String readSchemaDefinition(String schemaUri) throws CmsRuntimeException { 615 616 // the schema definition is located in a separate file for easier editing 617 String schemaDefinition; 618 try { 619 schemaDefinition = CmsFileUtil.readFile(schemaUri, CmsEncoder.ENCODING_UTF_8); 620 } catch (Exception e) { 621 throw new CmsRuntimeException( 622 Messages.get().container(Messages.ERR_XMLCONTENT_LOAD_SCHEMA_1, schemaUri), 623 e); 624 } 625 return schemaDefinition; 626 } 627}