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.xml.xml2json; 029 030import org.opencms.util.CmsStringUtil; 031import org.opencms.xml.CmsXmlContentDefinition; 032import org.opencms.xml.CmsXmlContentDefinition.SequenceType; 033import org.opencms.xml.CmsXmlUtils; 034import org.opencms.xml.content.CmsXmlContent; 035import org.opencms.xml.types.CmsXmlNestedContentDefinition; 036import org.opencms.xml.types.I_CmsXmlContentValue; 037import org.opencms.xml.types.I_CmsXmlSchemaType; 038 039import java.util.ArrayList; 040import java.util.Arrays; 041import java.util.Collections; 042import java.util.HashMap; 043import java.util.IdentityHashMap; 044import java.util.List; 045import java.util.Locale; 046import java.util.Map; 047import java.util.function.Consumer; 048 049import org.dom4j.Element; 050 051/** 052 * Tree representation of CmsXmlContent which is suitable for XML-to-JSON transformations. 053 */ 054public class CmsXmlContentTree { 055 056 /** 057 * Field of a complex type. 058 * 059 * <p> 060 * This represents one or more elements with the same field name in a sequence in an XML content. 061 */ 062 public class Field { 063 064 /** The field definition. */ 065 I_CmsXmlSchemaType m_fieldDef; 066 067 /** The nodes for the individual field elements. */ 068 List<Node> m_nodes; 069 070 /** The nested content definition (may be null). */ 071 private CmsXmlContentDefinition m_nestedDef; 072 073 /** The parent node of the field. */ 074 protected Node m_parentNode; 075 076 /** 077 * Create a new instance. 078 * 079 * @param fieldDef the field definition 080 * @param nodes the nodes for the individual field elements 081 */ 082 public Field(I_CmsXmlSchemaType fieldDef, List<Node> nodes) { 083 084 m_fieldDef = fieldDef; 085 if (fieldDef instanceof CmsXmlNestedContentDefinition) { 086 m_nestedDef = ((CmsXmlNestedContentDefinition)fieldDef).getNestedContentDefinition(); 087 } 088 m_nodes = nodes; 089 } 090 091 /** 092 * Gets the field definition. 093 * 094 * @return the field definition 095 */ 096 public I_CmsXmlSchemaType getFieldDefinition() { 097 098 return m_fieldDef; 099 } 100 101 /** 102 * Gets the field name. 103 * 104 * @return the field name 105 */ 106 public String getName() { 107 108 return m_fieldDef.getName(); 109 } 110 111 /** 112 * Gets the node if there is exactly one in the node list, otherwise throws an error. 113 * 114 * @return the node 115 */ 116 public Node getNode() { 117 118 if (m_nodes.size() != 1) { 119 throw new IllegalStateException( 120 "Can't call getNode for field with a number of nodes different from 1."); 121 } 122 return m_nodes.get(0); 123 } 124 125 /** 126 * Gets the nodes for the individual elements with that field name. 127 * 128 * @return the sub-nodes for this field 129 */ 130 public List<Node> getNodes() { 131 132 return m_nodes; 133 } 134 135 /** 136 * Gets the parent node. 137 * 138 * @return the parent node 139 */ 140 public Node getParentNode() { 141 142 return m_parentNode; 143 } 144 145 /** 146 * Returns true if the field refers to a choice element with maxOccurs greater than 1. 147 * 148 * @return true if this is a multichoice attribute 149 */ 150 public boolean isMultiChoice() { 151 152 return (m_nestedDef != null) && (m_nestedDef.getChoiceMaxOccurs() > 1); 153 154 } 155 156 /** 157 * Returns true if this is a multivalue field. 158 * 159 * @return true if this is a multivalue field 160 */ 161 public boolean isMultivalue() { 162 163 return m_fieldDef.getMaxOccurs() > 1; 164 } 165 166 /** 167 * Returns true if this is an optional field. 168 * 169 * @return true if this is an optional field 170 */ 171 public boolean isOptional() { 172 173 return m_fieldDef.getMinOccurs() == 0; 174 } 175 176 /** 177 * Sets the parent node of the field. 178 * 179 * @param parentNode the parent node 180 */ 181 public void setParentNode(Node parentNode) { 182 183 m_parentNode = parentNode; 184 } 185 186 /** 187 * @see java.lang.Object#toString() 188 */ 189 @Override 190 public String toString() { 191 192 String result = "FL " + m_fieldDef.getName() + ":\n"; 193 for (Node child : m_nodes) { 194 String entry = CmsStringUtil.indentLines(child.toString(), 4) + "\n"; 195 result += entry; 196 197 } 198 return result; 199 } 200 } 201 202 /** 203 * Represents a sequence in the XML content. 204 */ 205 public class Node { 206 207 /** The content definition. */ 208 private CmsXmlContentDefinition m_contentDefinition; 209 210 /** The underlying element. */ 211 private Element m_elem; 212 213 /** The list of sequence fields. */ 214 private List<Field> m_fields; 215 216 /** The node type. */ 217 private NodeType m_type; 218 219 /** The content value. */ 220 private I_CmsXmlContentValue m_value; 221 222 /** 223 * Creates a new instance. 224 * @param type the node type 225 * @param value the content value 226 * @param contentDef the content definition 227 * @param elem the underlying XML element 228 * @param fields the fields 229 */ 230 public Node( 231 NodeType type, 232 I_CmsXmlContentValue value, 233 CmsXmlContentDefinition contentDef, 234 Element elem, 235 List<Field> fields) { 236 237 m_type = type; 238 m_contentDefinition = contentDef; 239 m_elem = elem; 240 m_fields = fields; 241 m_value = value; 242 if (m_fields != null) { 243 for (Field field : m_fields) { 244 field.setParentNode(this); 245 } 246 } else { 247 m_fields = Collections.emptyList(); 248 } 249 } 250 251 /** 252 * Gets the content definition. 253 * 254 * @return the contnt definition 255 */ 256 public CmsXmlContentDefinition getContentDefinition() { 257 258 return m_contentDefinition; 259 } 260 261 /** 262 * Gets the DOM element for the node. 263 * 264 * @return the DOM element 265 */ 266 public Element getElement() { 267 268 return m_elem; 269 } 270 271 /** 272 * Gets the fields for the sequence. 273 * 274 * @return the list of fields 275 */ 276 public List<Field> getFields() { 277 278 return m_fields; 279 } 280 281 /** 282 * Gets the path of the node. 283 * 284 * @return the path of the node 285 */ 286 @SuppressWarnings("synthetic-access") 287 public String getPath() { 288 289 return getValuePath(m_elem); 290 } 291 292 /** 293 * Gets the node type. 294 * 295 * @return the node type 296 */ 297 public NodeType getType() { 298 299 return m_type; 300 } 301 302 /** 303 * Gets the content value (null for root node). 304 * 305 * @return the content value 306 */ 307 public I_CmsXmlContentValue getValue() { 308 309 return m_value; 310 } 311 312 /** 313 * @see java.lang.Object#toString() 314 */ 315 @Override 316 public String toString() { 317 318 StringBuilder buffer = new StringBuilder(); 319 buffer.append("ND:\n"); 320 for (Field field : m_fields) { 321 buffer.append(CmsStringUtil.indentLines(field.toString(), 4)); 322 } 323 return buffer.toString(); 324 325 } 326 327 } 328 329 /** 330 * Enum representing the type of the tree node. 331 */ 332 public enum NodeType { 333 /** Complex value: choice. */ 334 choice, 335 /** Complex value: sequence. */ 336 sequence, 337 338 /** Simple value. */ 339 simple; 340 } 341 342 /** Map from values to nodes. */ 343 private IdentityHashMap<I_CmsXmlContentValue, Node> m_valueToNodeCache = new IdentityHashMap<>(); 344 345 /** The content. */ 346 private CmsXmlContent m_content; 347 348 /** The locale for which the tree should be generated. */ 349 private Locale m_locale; 350 351 /** The root node. */ 352 private Node m_root; 353 354 /** 355 * Creates a new instance and initializes the full tree for the given locale. 356 * 357 * @param content the content from which the tree should be generated 358 * @param locale the locale for which the tree should be generated 359 */ 360 public CmsXmlContentTree(CmsXmlContent content, Locale locale) { 361 362 m_content = content; 363 m_locale = locale; 364 m_root = createNode(content.getLocaleNode(locale), content.getContentDefinition()); 365 visitNodes(m_root, node -> { 366 if (node.getValue() != null) { 367 m_valueToNodeCache.put(node.getValue(), node); 368 } 369 }); 370 } 371 372 /** 373 * Visits all Node instances that are descendants of a given node (including that node itself). 374 * 375 * @param node the root node 376 * @param handler the handler to be invoked for all descendant nodes 377 */ 378 public static void visitNodes(Node node, Consumer<Node> handler) { 379 380 handler.accept(node); 381 for (Field field : node.getFields()) { 382 for (Node child : field.getNodes()) { 383 visitNodes(child, handler); 384 } 385 } 386 } 387 388 /** 389 * Creates a node for the given content definition and DOM element. 390 * 391 * @param elem the XML DOM element 392 * @param contentDef the content definition (null for non-nested values) 393 * 394 * @return the created node 395 */ 396 public Node createNode(Element elem, CmsXmlContentDefinition contentDef) { 397 398 String path = getValuePath(elem); 399 I_CmsXmlContentValue value = path.isEmpty() ? null : m_content.getValue(path, m_locale); 400 if (contentDef == null) { 401 Node node = new Node(NodeType.simple, value, null, elem, null); 402 return node; 403 } 404 405 SequenceType seqType = contentDef.getSequenceType(); 406 if ((seqType == SequenceType.MULTIPLE_CHOICE) || (seqType == SequenceType.SINGLE_CHOICE)) { 407 List<I_CmsXmlSchemaType> fieldDefinitions = contentDef.getTypeSequence(); 408 Map<String, I_CmsXmlSchemaType> defMap = new HashMap<>(); 409 for (I_CmsXmlSchemaType fieldDef : fieldDefinitions) { 410 String fieldName = fieldDef.getName(); 411 defMap.put(fieldName, fieldDef); 412 } 413 List<Field> choiceFields = new ArrayList<>(); 414 for (Element child : elem.elements()) { 415 I_CmsXmlSchemaType childFieldDef = defMap.get(child.getName()); 416 CmsXmlContentDefinition nestedDef = null; 417 if (childFieldDef instanceof CmsXmlNestedContentDefinition) { 418 CmsXmlNestedContentDefinition nestedDefType = (CmsXmlNestedContentDefinition)childFieldDef; 419 nestedDef = nestedDefType.getNestedContentDefinition(); 420 } 421 Node childNode = createNode(child, nestedDef); 422 Field choiceField = new Field(childFieldDef, Arrays.asList(childNode)); 423 choiceFields.add(choiceField); 424 } 425 Node node = new Node(NodeType.choice, value, contentDef, elem, choiceFields); 426 return node; 427 } 428 429 if (seqType == SequenceType.SEQUENCE) { 430 List<I_CmsXmlSchemaType> fieldDefinitions = contentDef.getTypeSequence(); 431 List<Field> fields = new ArrayList<>(); 432 for (I_CmsXmlSchemaType fieldDef : fieldDefinitions) { 433 CmsXmlContentDefinition nestedDef = null; 434 if (fieldDef instanceof CmsXmlNestedContentDefinition) { 435 CmsXmlNestedContentDefinition nestedDefType = (CmsXmlNestedContentDefinition)fieldDef; 436 nestedDef = nestedDefType.getNestedContentDefinition(); 437 } 438 String fieldName = fieldDef.getName(); 439 String fieldPath = CmsXmlUtils.concatXpath(path, fieldName); 440 List<I_CmsXmlContentValue> fieldValues = m_content.getValues(fieldPath, m_locale); 441 List<Node> fieldChildren = new ArrayList<>(); 442 for (int i = 0; i < fieldValues.size(); i++) { 443 Element subElement = fieldValues.get(i).getElement(); 444 Node fieldChild = createNode(subElement, nestedDef); 445 fieldChildren.add(fieldChild); 446 } 447 Field field = new Field(fieldDef, fieldChildren); 448 fields.add(field); 449 } 450 Node seqNode = new Node(NodeType.sequence, value, contentDef, elem, fields); 451 return seqNode; 452 } 453 throw new IllegalStateException( 454 "Invalid content definition type encounterered while processing " + m_content.getFile().getRootPath()); 455 } 456 457 /** 458 * Gets the node corresponding to the given value. 459 * 460 * @param value a content value 461 * @return the node for the value, or null if no node is found 462 */ 463 public Node getNodeForValue(I_CmsXmlContentValue value) { 464 465 return m_valueToNodeCache.get(value); 466 } 467 468 /** 469 * Returns the root node. 470 * 471 * @return the root node 472 */ 473 public Node getRoot() { 474 475 return m_root; 476 } 477 478 /** 479 * @see java.lang.Object#toString() 480 */ 481 @Override 482 public String toString() { 483 484 return m_content.getFile().getRootPath() + ":\n" + CmsStringUtil.indentLines(m_root.toString(), 4); 485 } 486 487 /** 488 * Gets a path for an element which can be fed to CmsXmlContent.getValue(). 489 * 490 * @param element the element 491 * @return the path 492 */ 493 private String getValuePath(Element element) { 494 495 String fullPath = element.getUniquePath(); 496 String prefix = m_content.getLocaleNode(m_locale).getUniquePath(); 497 String result = fullPath.substring(prefix.length()); 498 if (result.startsWith("/")) { 499 result = result.substring(1); 500 } 501 return result; 502 } 503 504}