001/** 002 * Copyright 2005-2018 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.kuali.rice.krad.datadictionary.parse; 017 018import com.sun.org.apache.xml.internal.serialize.Method; 019import com.sun.org.apache.xml.internal.serialize.OutputFormat; 020import com.sun.org.apache.xml.internal.serialize.XMLSerializer; 021import org.apache.commons.lang.StringUtils; 022import org.apache.commons.logging.Log; 023import org.apache.commons.logging.LogFactory; 024import org.springframework.beans.factory.config.BeanDefinition; 025import org.springframework.beans.factory.config.BeanDefinitionHolder; 026import org.springframework.beans.factory.config.RuntimeBeanReference; 027import org.springframework.beans.factory.support.BeanDefinitionBuilder; 028import org.springframework.beans.factory.support.ManagedList; 029import org.springframework.beans.factory.support.ManagedMap; 030import org.springframework.beans.factory.support.ManagedSet; 031import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; 032import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate; 033import org.springframework.beans.factory.xml.ParserContext; 034import org.springframework.util.xml.DomUtils; 035import org.w3c.dom.Document; 036import org.w3c.dom.Element; 037import org.w3c.dom.NamedNodeMap; 038import org.w3c.dom.Node; 039import org.w3c.dom.NodeList; 040 041import java.io.IOException; 042import java.io.StringWriter; 043import java.util.ArrayList; 044import java.util.Map; 045import java.util.Set; 046 047/** 048 * Parser for parsing xml bean's created using the custom schema into normal spring bean format. 049 * 050 * @author Kuali Rice Team (rice.collab@kuali.org) 051 */ 052public class CustomSchemaParser extends AbstractSingleBeanDefinitionParser { 053 private static final Log LOG = LogFactory.getLog(CustomSchemaParser.class); 054 055 private static final String INC_TAG = "inc"; 056 057 private static int beanNumber = 0; 058 059 /** 060 * Retrieves the class of the bean defined by the xml element. 061 * 062 * @param bean the xml element for the bean being parsed 063 * @return the class associated with the provided tag 064 */ 065 @Override 066 protected Class<?> getBeanClass(Element bean) { 067 Map<String, BeanTagInfo> beanType = CustomTagAnnotations.getBeanTags(); 068 069 if (!beanType.containsKey(bean.getLocalName())) { 070 return null; 071 } 072 073 // retrieve the connected class in the tag map using the xml tag's name. 074 return beanType.get(bean.getLocalName()).getBeanClass(); 075 } 076 077 /** 078 * Parses the xml bean into a standard bean definition format and fills the information in the passed in definition 079 * builder 080 * 081 * @param element - The xml bean being parsed. 082 * @param parserContext - Provided information and functionality regarding current bean set. 083 * @param bean - A definition builder used to build a new spring bean from the information it is filled with. 084 */ 085 @Override 086 protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder bean) { 087 // Retrieve custom schema information build from the annotations 088 Map<String, Map<String, BeanTagAttributeInfo>> attributeProperties = 089 CustomTagAnnotations.getAttributeProperties(); 090 Map<String, BeanTagAttributeInfo> entries = attributeProperties.get(element.getLocalName()); 091 092 // Log error if there are no attributes found for the bean tag 093 if (entries == null) { 094 LOG.error("Bean Tag not found " + element.getLocalName()); 095 } 096 097 if (element.getTagName().equals(INC_TAG)) { 098 String parentId = element.getAttribute("compId"); 099 bean.setParentName(parentId); 100 101 return; 102 } 103 104 if (element.getTagName().equals("content")) { 105 bean.setParentName("Uif-Content"); 106 107 String markup = nodesToString(element.getChildNodes()); 108 bean.addPropertyValue("markup", markup); 109 110 return; 111 } 112 113 // Retrieve the information for the new bean tag and fill in the default parent if needed 114 BeanTagInfo tagInfo = CustomTagAnnotations.getBeanTags().get(element.getLocalName()); 115 116 String elementParent = element.getAttribute("parent"); 117 if (StringUtils.isNotBlank(elementParent) && !StringUtils.equals(elementParent, tagInfo.getParent())) { 118 bean.setParentName(elementParent); 119 } else if (StringUtils.isNotBlank(tagInfo.getParent())) { 120 bean.setParentName(tagInfo.getParent()); 121 } 122 123 // Create the map for the attributes found in the tag and process them in to the definition builder. 124 NamedNodeMap attributes = element.getAttributes(); 125 for (int i = 0; i < attributes.getLength(); i++) { 126 processSingleValue(attributes.item(i).getNodeName(), attributes.item(i).getNodeValue(), entries, bean); 127 } 128 129 ArrayList<Element> children = (ArrayList<Element>) DomUtils.getChildElements(element); 130 131 // Process the children found in the xml tag 132 for (int i = 0; i < children.size(); i++) { 133 String tag = children.get(i).getLocalName(); 134 BeanTagAttributeInfo info = entries.get(tag); 135 136 if (children.get(i).getTagName().equals("spring:property") || children.get(i).getTagName().equals( 137 "property")) { 138 BeanDefinitionParserDelegate delegate = parserContext.getDelegate(); 139 delegate.parsePropertyElement(children.get(i), bean.getBeanDefinition()); 140 141 continue; 142 } 143 144 // Sets the property name to be used when adding the property value 145 String propertyName; 146 BeanTagAttribute.AttributeType type = null; 147 if (info == null) { 148 propertyName = CustomTagAnnotations.findPropertyByType(element.getLocalName(), tag); 149 150 if (StringUtils.isNotBlank(propertyName)) { 151 bean.addPropertyValue(propertyName, parseBean(children.get(i), bean, parserContext)); 152 153 continue; 154 } else { 155 // If the tag is not in the schema map let spring handle the value by forwarding the tag as the 156 // propertyName 157 propertyName = tag; 158 type = findBeanType(children.get(i)); 159 } 160 } else { 161 // If the tag is found in the schema map use the connected name stored in the attribute information 162 propertyName = info.getPropertyName(); 163 type = info.getType(); 164 } 165 166 // Process the information stored in the child bean 167 ArrayList<Element> grandChildren = (ArrayList<Element>) DomUtils.getChildElements(children.get(i)); 168 169 if (type == BeanTagAttribute.AttributeType.SINGLEVALUE) { 170 String propertyValue = DomUtils.getTextValue(children.get(i)); 171 bean.addPropertyValue(propertyName, propertyValue); 172 } else if (type == BeanTagAttribute.AttributeType.ANY) { 173 String propertyValue = nodesToString(children.get(i).getChildNodes()); 174 bean.addPropertyValue(propertyName, propertyValue); 175 } else if ((type == BeanTagAttribute.AttributeType.DIRECT) || (type 176 == BeanTagAttribute.AttributeType.DIRECTORBYTYPE)) { 177 boolean isPropertyTag = false; 178 if ((children.get(i).getAttributes().getLength() == 0) && (grandChildren.size() == 1)) { 179 String grandChildTag = grandChildren.get(0).getLocalName(); 180 181 Class<?> valueClass = info.getValueType(); 182 if (valueClass.isInterface()) { 183 try { 184 valueClass = Class.forName(valueClass.getName() + "Base"); 185 } catch (ClassNotFoundException e) { 186 throw new RuntimeException("Unable to find impl class for interface", e); 187 } 188 } 189 190 Set<String> validTagNames = CustomTagAnnotations.getBeanTagsByClass(valueClass); 191 if (validTagNames.contains(grandChildTag)) { 192 isPropertyTag = true; 193 } 194 } 195 196 if (isPropertyTag) { 197 bean.addPropertyValue(propertyName, parseBean(grandChildren.get(0), bean, parserContext)); 198 } else { 199 bean.addPropertyValue(propertyName, parseBean(children.get(i), bean, parserContext)); 200 } 201 } else if ((type == BeanTagAttribute.AttributeType.SINGLEBEAN) || (type 202 == BeanTagAttribute.AttributeType.BYTYPE)) { 203 bean.addPropertyValue(propertyName, parseBean(grandChildren.get(0), bean, parserContext)); 204 } else if (type == BeanTagAttribute.AttributeType.LISTBEAN) { 205 bean.addPropertyValue(propertyName, parseList(grandChildren, children.get(i), bean, parserContext)); 206 } else if (type == BeanTagAttribute.AttributeType.LISTVALUE) { 207 bean.addPropertyValue(propertyName, parseList(grandChildren, children.get(i), bean, parserContext)); 208 } else if (type == BeanTagAttribute.AttributeType.MAPVALUE) { 209 bean.addPropertyValue(propertyName, parseMap(grandChildren, children.get(i), bean, parserContext)); 210 } else if (type == BeanTagAttribute.AttributeType.MAPBEAN) { 211 bean.addPropertyValue(propertyName, parseMap(grandChildren, children.get(i), bean, parserContext)); 212 } else if (type == BeanTagAttribute.AttributeType.SETVALUE) { 213 bean.addPropertyValue(propertyName, parseSet(grandChildren, children.get(i), bean, parserContext)); 214 } else if (type == BeanTagAttribute.AttributeType.SETBEAN) { 215 bean.addPropertyValue(propertyName, parseSet(grandChildren, children.get(i), bean, parserContext)); 216 } 217 } 218 } 219 220 /** 221 * Adds the property value to the bean definition based on the name and value of the attribute. 222 * 223 * @param name - The name of the attribute. 224 * @param value - The value of the attribute. 225 * @param entries - The property entries for the over all tag. 226 * @param bean - The bean definition being created. 227 */ 228 protected void processSingleValue(String name, String value, Map<String, BeanTagAttributeInfo> entries, 229 BeanDefinitionBuilder bean) { 230 231 if (name.toLowerCase().compareTo("parent") == 0) { 232 // If attribute is defining the parent set it in the bean builder. 233 bean.setParentName(value); 234 } else if (name.toLowerCase().compareTo("abstract") == 0) { 235 // If the attribute is defining the parent as abstract set it in the bean builder. 236 bean.setAbstract(Boolean.valueOf(value)); 237 } else if (name.toLowerCase().compareTo("id") == 0) { 238 //nothing - insures that its erased 239 } else { 240 // If the attribute is not a reserved case find the property name form the connected map and add the new 241 // property value. 242 243 if (name.contains("-ref")) { 244 bean.addPropertyValue(name.substring(0, name.length() - 4), new RuntimeBeanReference(value)); 245 } else { 246 BeanTagAttributeInfo info = entries.get(name); 247 String propertyName; 248 249 if (info == null) { 250 propertyName = name; 251 } else { 252 propertyName = info.getName(); 253 } 254 bean.addPropertyValue(propertyName, value); 255 } 256 } 257 } 258 259 /** 260 * Finds the key of a map entry in the custom schema. 261 * 262 * @param grandchild - The map entry. 263 * @return The object (bean or value) entry key 264 */ 265 protected Object findKey(Element grandchild, BeanDefinitionBuilder parent, ParserContext parserContext) { 266 String key = grandchild.getAttribute("key"); 267 if (!key.isEmpty()) { 268 return key; 269 } 270 271 Element keyTag = DomUtils.getChildElementByTagName(grandchild, "key"); 272 if (keyTag != null) { 273 if (DomUtils.getChildElements(keyTag).isEmpty()) { 274 return keyTag.getTextContent(); 275 } else { 276 return parseBean(DomUtils.getChildElements(keyTag).get(0), parent, parserContext); 277 } 278 } 279 280 return null; 281 } 282 283 /** 284 * Finds the value of a map entry in the custom schema. 285 * 286 * @param grandchild - The map entry. 287 * @return The object (bean or value) entry value 288 */ 289 protected Object findValue(Element grandchild, BeanDefinitionBuilder parent, ParserContext parserContext) { 290 String value = grandchild.getAttribute("value"); 291 if (!value.isEmpty()) { 292 return value; 293 } 294 295 Element valueTag = DomUtils.getChildElementByTagName(grandchild, "value"); 296 if (valueTag != null) { 297 if (DomUtils.getChildElements(valueTag).isEmpty()) { 298 return valueTag.getTextContent(); 299 } else { 300 return parseBean(DomUtils.getChildElements(valueTag).get(0), parent, parserContext); 301 } 302 } 303 304 return null; 305 } 306 307 /** 308 * Finds the attribute type of the schema being used by the element. 309 * 310 * @param tag - The tag to check. 311 * @return The schema attribute type. 312 */ 313 protected BeanTagAttribute.AttributeType findBeanType(Element tag) { 314 int numberChildren = 0; 315 316 // Checks if the user overrides the default attribute type of the schema. 317 String overrideType = tag.getAttribute("overrideBeanType"); 318 if (!StringUtils.isEmpty(overrideType)) { 319 if (overrideType.toLowerCase().compareTo("singlebean") == 0) { 320 return BeanTagAttribute.AttributeType.SINGLEBEAN; 321 } 322 if (overrideType.toLowerCase().compareTo("singlevalue") == 0) { 323 return BeanTagAttribute.AttributeType.SINGLEVALUE; 324 } 325 if (overrideType.toLowerCase().compareTo("listbean") == 0) { 326 return BeanTagAttribute.AttributeType.LISTBEAN; 327 } 328 if (overrideType.toLowerCase().compareTo("listvalue") == 0) { 329 return BeanTagAttribute.AttributeType.LISTVALUE; 330 } 331 if (overrideType.toLowerCase().compareTo("mapbean") == 0) { 332 return BeanTagAttribute.AttributeType.MAPBEAN; 333 } 334 if (overrideType.toLowerCase().compareTo("mapvalue") == 0) { 335 return BeanTagAttribute.AttributeType.MAPVALUE; 336 } 337 if (overrideType.toLowerCase().compareTo("setbean") == 0) { 338 return BeanTagAttribute.AttributeType.SETBEAN; 339 } 340 if (overrideType.toLowerCase().compareTo("setvalue") == 0) { 341 return BeanTagAttribute.AttributeType.SETVALUE; 342 } 343 } 344 345 // Checks if the element is a list composed of standard types 346 numberChildren = DomUtils.getChildElementsByTagName(tag, "value").size(); 347 if (numberChildren > 0) { 348 return BeanTagAttribute.AttributeType.LISTVALUE; 349 } 350 351 numberChildren = DomUtils.getChildElementsByTagName(tag, "spring:list").size(); 352 if (numberChildren > 0) { 353 return BeanTagAttribute.AttributeType.LISTBEAN; 354 } 355 356 numberChildren = DomUtils.getChildElementsByTagName(tag, "spring:set").size(); 357 if (numberChildren > 0) { 358 return BeanTagAttribute.AttributeType.SETBEAN; 359 } 360 361 // Checks if the element is a map 362 numberChildren = DomUtils.getChildElementsByTagName(tag, "entry").size(); 363 if (numberChildren > 0) { 364 return BeanTagAttribute.AttributeType.MAPVALUE; 365 } 366 367 numberChildren = DomUtils.getChildElementsByTagName(tag, "map").size(); 368 if (numberChildren > 0) { 369 return BeanTagAttribute.AttributeType.MAPBEAN; 370 } 371 372 // Checks if the element is a list of beans 373 numberChildren = DomUtils.getChildElements(tag).size(); 374 if (numberChildren > 1) { 375 return BeanTagAttribute.AttributeType.LISTBEAN; 376 } 377 378 // Defaults to return the element as a single bean. 379 return BeanTagAttribute.AttributeType.SINGLEBEAN; 380 } 381 382 /** 383 * Parses a bean based on the namespace of the bean. 384 * 385 * @param tag - The Element to be parsed. 386 * @param parent - The parent bean that the tag is nested in. 387 * @param parserContext - Provided information and functionality regarding current bean set. 388 * @return The parsed bean. 389 */ 390 protected Object parseBean(Element tag, BeanDefinitionBuilder parent, ParserContext parserContext) { 391 if (tag.getNamespaceURI().compareTo("http://www.springframework.org/schema/beans") == 0 || tag.getLocalName() 392 .equals("bean")) { 393 return parseSpringBean(tag, parserContext); 394 } else { 395 return parseCustomBean(tag, parent, parserContext); 396 } 397 } 398 399 /** 400 * Parses a bean of the spring namespace. 401 * 402 * @param tag - The Element to be parsed. 403 * @return The parsed bean. 404 */ 405 protected Object parseSpringBean(Element tag, ParserContext parserContext) { 406 if (tag.getLocalName().compareTo("ref") == 0) { 407 // Create the referenced bean by creating a new bean and setting its parent to the referenced bean 408 // then replace grand child with it 409 Element temp = tag.getOwnerDocument().createElement("bean"); 410 temp.setAttribute("parent", tag.getAttribute("bean")); 411 tag = temp; 412 return new RuntimeBeanReference(tag.getAttribute("parent")); 413 } 414 415 //peel off p: properties an make them actual property nodes - p-namespace does not work properly (unknown cause) 416 Document document = tag.getOwnerDocument(); 417 NamedNodeMap attributes = tag.getAttributes(); 418 for (int i = 0; i < attributes.getLength(); i++) { 419 Node attribute = attributes.item(i); 420 String name = attribute.getNodeName(); 421 if (name.startsWith("p:")) { 422 Element property = document.createElement("property"); 423 property.setAttribute("name", StringUtils.removeStart(name, "p:")); 424 property.setAttribute("value", attribute.getTextContent()); 425 426 if (tag.getFirstChild() != null) { 427 tag.insertBefore(property, tag.getFirstChild()); 428 } else { 429 tag.appendChild(property); 430 } 431 } 432 } 433 434 // Create the bean definition for the grandchild and return it. 435 BeanDefinitionParserDelegate delegate = parserContext.getDelegate(); 436 BeanDefinitionHolder bean = delegate.parseBeanDefinitionElement(tag); 437 438 // Creates a custom name for the new bean. 439 String name = bean.getBeanDefinition().getParentName() + "$Customchild" + beanNumber; 440 if (tag.getAttribute("id") != null && !StringUtils.isEmpty(tag.getAttribute("id"))) { 441 name = tag.getAttribute("id"); 442 } else { 443 beanNumber++; 444 } 445 446 return new BeanDefinitionHolder(bean.getBeanDefinition(), name); 447 } 448 449 /** 450 * Parses a bean of the custom namespace. 451 * 452 * @param tag - The Element to be parsed. 453 * @param parent - The parent bean that the tag is nested in. 454 * @param parserContext - Provided information and functionality regarding current bean set. 455 * @return The parsed bean. 456 */ 457 protected Object parseCustomBean(Element tag, BeanDefinitionBuilder parent, ParserContext parserContext) { 458 BeanDefinition beanDefinition = parserContext.getDelegate().parseCustomElement(tag, parent.getBeanDefinition()); 459 460 String name = beanDefinition.getParentName() + "$Customchild" + beanNumber; 461 if (tag.getAttribute("id") != null && !StringUtils.isEmpty(tag.getAttribute("id"))) { 462 name = tag.getAttribute("id"); 463 } else { 464 beanNumber++; 465 } 466 467 return new BeanDefinitionHolder(beanDefinition, name); 468 } 469 470 /** 471 * Parses a list of elements into a list of beans/standard content. 472 * 473 * @param grandChildren - The list of beans/content in a bean property 474 * @param child - The property tag for the parent. 475 * @param parent - The parent bean that the tag is nested in. 476 * @param parserContext - Provided information and functionality regarding current bean set. 477 * @return A managedList of the nested content. 478 */ 479 protected ManagedList parseList(ArrayList<Element> grandChildren, Element child, BeanDefinitionBuilder parent, 480 ParserContext parserContext) { 481 ArrayList<Object> listItems = new ArrayList<Object>(); 482 483 for (int i = 0; i < grandChildren.size(); i++) { 484 Element grandChild = grandChildren.get(i); 485 486 if (grandChild.getTagName().compareTo("value") == 0) { 487 listItems.add(grandChild.getTextContent()); 488 } else { 489 listItems.add(parseBean(grandChild, parent, parserContext)); 490 } 491 } 492 493 String merge = child.getAttribute("merge"); 494 495 ManagedList beans = new ManagedList(listItems.size()); 496 beans.addAll(listItems); 497 498 if (merge != null) { 499 beans.setMergeEnabled(Boolean.valueOf(merge)); 500 } 501 502 return beans; 503 } 504 505 /** 506 * Parses a list of elements into a set of beans/standard content. 507 * 508 * @param grandChildren - The set of beans/content in a bean property 509 * @param child - The property tag for the parent. 510 * @param parent - The parent bean that the tag is nested in. 511 * @param parserContext - Provided information and functionality regarding current bean set. 512 * @return A managedSet of the nested content. 513 */ 514 protected ManagedSet parseSet(ArrayList<Element> grandChildren, Element child, BeanDefinitionBuilder parent, 515 ParserContext parserContext) { 516 ManagedSet setItems = new ManagedSet(); 517 518 for (int i = 0; i < grandChildren.size(); i++) { 519 Element grandChild = grandChildren.get(i); 520 521 if (child.getTagName().compareTo("value") == 0) { 522 setItems.add(grandChild.getTextContent()); 523 } else { 524 setItems.add(parseBean(grandChild, parent, parserContext)); 525 } 526 } 527 528 String merge = child.getAttribute("merge"); 529 if (merge != null) { 530 setItems.setMergeEnabled(Boolean.valueOf(merge)); 531 } 532 533 return setItems; 534 } 535 536 /** 537 * Parses a list of elements into a map of beans/standard content. 538 * 539 * @param grandChildren - The list of beans/content in a bean property 540 * @param child - The property tag for the parent. 541 * @param parent - The parent bean that the tag is nested in. 542 * @param parserContext - Provided information and functionality regarding current bean set. 543 * @return A managedSet of the nested content. 544 */ 545 protected ManagedMap parseMap(ArrayList<Element> grandChildren, Element child, BeanDefinitionBuilder parent, 546 ParserContext parserContext) { 547 ManagedMap map = new ManagedMap(); 548 549 String merge = child.getAttribute("merge"); 550 if (merge != null) { 551 map.setMergeEnabled(Boolean.valueOf(merge)); 552 } 553 554 for (int j = 0; j < grandChildren.size(); j++) { 555 Object key = findKey(grandChildren.get(j), parent, parserContext); 556 Object value = findValue(grandChildren.get(j), parent, parserContext); 557 558 map.put(key, value); 559 } 560 561 return map; 562 } 563 564 protected String nodesToString(NodeList nodeList) { 565 StringBuffer sb = new StringBuffer(); 566 567 for (int i = 0; i < nodeList.getLength(); i++) { 568 Node node = nodeList.item(i); 569 570 sb.append(nodeToString(node)); 571 } 572 573 return sb.toString(); 574 } 575 576 protected String nodeToString(Node node) { 577 StringWriter stringOut = new StringWriter(); 578 579 OutputFormat format = new OutputFormat(Method.XML, null, false); 580 format.setOmitXMLDeclaration(true); 581 582 XMLSerializer serial = new XMLSerializer(stringOut, format); 583 584 try { 585 serial.serialize(node); 586 } catch (IOException e) { 587 throw new RuntimeException(e); 588 } 589 590 return stringOut.toString(); 591 } 592}