View Javadoc
1   /**
2    * Copyright 2005-2015 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.datadictionary.parse;
17  
18  import com.sun.org.apache.xml.internal.serialize.Method;
19  import com.sun.org.apache.xml.internal.serialize.OutputFormat;
20  import com.sun.org.apache.xml.internal.serialize.XMLSerializer;
21  import org.apache.commons.lang.StringUtils;
22  import org.apache.commons.logging.Log;
23  import org.apache.commons.logging.LogFactory;
24  import org.springframework.beans.factory.config.BeanDefinition;
25  import org.springframework.beans.factory.config.BeanDefinitionHolder;
26  import org.springframework.beans.factory.config.RuntimeBeanReference;
27  import org.springframework.beans.factory.support.BeanDefinitionBuilder;
28  import org.springframework.beans.factory.support.ManagedList;
29  import org.springframework.beans.factory.support.ManagedMap;
30  import org.springframework.beans.factory.support.ManagedSet;
31  import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
32  import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate;
33  import org.springframework.beans.factory.xml.ParserContext;
34  import org.springframework.util.xml.DomUtils;
35  import org.w3c.dom.Document;
36  import org.w3c.dom.Element;
37  import org.w3c.dom.NamedNodeMap;
38  import org.w3c.dom.Node;
39  import org.w3c.dom.NodeList;
40  
41  import java.io.IOException;
42  import java.io.StringWriter;
43  import java.util.ArrayList;
44  import java.util.Map;
45  import java.util.Set;
46  
47  /**
48   * Parser for parsing xml bean's created using the custom schema into normal spring bean format.
49   *
50   * @author Kuali Rice Team (rice.collab@kuali.org)
51   */
52  public class CustomSchemaParser extends AbstractSingleBeanDefinitionParser {
53      private static final Log LOG = LogFactory.getLog(CustomSchemaParser.class);
54  
55      private static final String INC_TAG = "inc";
56  
57      private static int beanNumber = 0;
58  
59      /**
60       * Retrieves the class of the bean defined by the xml element.
61       *
62       * @param bean the xml element for the bean being parsed
63       * @return the class associated with the provided tag
64       */
65      @Override
66      protected Class<?> getBeanClass(Element bean) {
67          Map<String, BeanTagInfo> beanType = CustomTagAnnotations.getBeanTags();
68  
69          if (!beanType.containsKey(bean.getLocalName())) {
70              return null;
71          }
72  
73          // retrieve the connected class in the tag map using the xml tag's name.
74          return beanType.get(bean.getLocalName()).getBeanClass();
75      }
76  
77      /**
78       * Parses the xml bean into a standard bean definition format and fills the information in the passed in definition
79       * builder
80       *
81       * @param element - The xml bean being parsed.
82       * @param parserContext - Provided information and functionality regarding current bean set.
83       * @param bean - A definition builder used to build a new spring bean from the information it is filled with.
84       */
85      @Override
86      protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder bean) {
87          // Retrieve custom schema information build from the annotations
88          Map<String, Map<String, BeanTagAttributeInfo>> attributeProperties =
89                  CustomTagAnnotations.getAttributeProperties();
90          Map<String, BeanTagAttributeInfo> entries = attributeProperties.get(element.getLocalName());
91  
92          // Log error if there are no attributes found for the bean tag
93          if (entries == null) {
94              LOG.error("Bean Tag not found " + element.getLocalName());
95          }
96  
97          if (element.getTagName().equals(INC_TAG)) {
98              String parentId = element.getAttribute("compId");
99              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 }