View Javadoc
1   /**
2    * Copyright 2005-2014 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 org.apache.commons.lang.StringUtils;
19  import org.apache.commons.logging.Log;
20  import org.apache.commons.logging.LogFactory;
21  import org.springframework.beans.factory.config.BeanDefinition;
22  import org.springframework.beans.factory.config.BeanDefinitionHolder;
23  import org.springframework.beans.factory.config.RuntimeBeanReference;
24  import org.springframework.beans.factory.support.BeanDefinitionBuilder;
25  import org.springframework.beans.factory.support.ManagedList;
26  import org.springframework.beans.factory.support.ManagedMap;
27  import org.springframework.beans.factory.support.ManagedSet;
28  import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
29  import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate;
30  import org.springframework.beans.factory.xml.ParserContext;
31  import org.springframework.util.xml.DomUtils;
32  import org.w3c.dom.Document;
33  import org.w3c.dom.Element;
34  import org.w3c.dom.NamedNodeMap;
35  import org.w3c.dom.Node;
36  
37  import java.util.ArrayList;
38  import java.util.Map;
39  
40  /**
41   * Parser for parsing xml bean's created using the custom schema into normal spring bean format.
42   *
43   * @author Kuali Rice Team (rice.collab@kuali.org)
44   */
45  public class CustomSchemaParser extends AbstractSingleBeanDefinitionParser {
46      private static final Log LOG = LogFactory.getLog(CustomSchemaParser.class);
47  
48      private static int beanNumber = 0;
49  
50      /**
51       * Retrieves the class of the bean defined by the xml element.
52       *
53       * @param bean - The xml element for the bean being parsed.
54       * @return The class associated with the provided tag
55       */
56      protected Class getBeanClass(Element bean) {
57          Map<String, BeanTagInfo> beanType = null;
58  
59          // Attempt to load the list of tags
60          try {
61              beanType = CustomTagAnnotations.getBeanTags();
62          } catch (Exception e) {
63              LOG.error("Error retrieving bean tag information", e);
64          }
65  
66          Class<?> beanTag = null;
67          try {
68              // Retrieve the connected class in the tag map using the xml tag's name.
69  
70              beanTag = beanType.get(bean.getLocalName()).getBeanClass();
71          } catch (Exception e) {
72              LOG.error("Error in retrieved bean tag information", e);
73          }
74  
75          return beanTag;
76      }
77  
78      /**
79       * Parses the xml bean into a standard bean definition format and fills the information in the passed in definition
80       * builder
81       *
82       * @param element - The xml bean being parsed.
83       * @param parserContext - Provided information and functionality regarding current bean set.
84       * @param bean - A definition builder used to build a new spring bean from the information it is filled with.
85       */
86      @Override
87      protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder bean) {
88          // Retrieve custom schema information build from the annotations
89          Map<String, Map<String, BeanTagAttributeInfo>> attributeProperties =
90                  CustomTagAnnotations.getAttributeProperties();
91          Map<String, BeanTagAttributeInfo> entries = attributeProperties.get(element.getLocalName());
92  
93          // Log error if there are no attributes found for the bean tag
94          if (entries == null) {
95              LOG.error("Bean Tag not found " + element.getLocalName());
96          }
97  
98          // Retrieve the information for the new bean tag and fill in the default parent if needed
99          BeanTagInfo tagInfo = CustomTagAnnotations.getBeanTags().get(element.getLocalName());
100         if (tagInfo.getParent().compareTo("none") != 0) {
101             bean.setParentName(tagInfo.getParent());
102         }
103 
104         // Create the map for the attributes found in the tag and process them in to the definition builder.
105         NamedNodeMap attributes = element.getAttributes();
106         for (int i = 0; i < attributes.getLength(); i++) {
107             processSingleValue(attributes.item(i).getNodeName(), attributes.item(i).getNodeValue(), entries, bean);
108         }
109 
110         ArrayList<Element> children = (ArrayList<Element>) DomUtils.getChildElements(element);
111 
112         // Process the children found in the xml tag
113         for (int i = 0; i < children.size(); i++) {
114             String tag = children.get(i).getLocalName();
115             BeanTagAttributeInfo info = entries.get(tag);
116 
117             String propertyName;
118             BeanTagAttribute.AttributeType type;
119 
120             if(children.get(i).getTagName().equals("spring:property")){
121                 BeanDefinitionParserDelegate delegate = parserContext.getDelegate();
122                 delegate.parsePropertyElement(children.get(i), bean.getBeanDefinition());
123                 continue;
124             }
125 
126             // Sets the property name to be used when adding the property value
127             if (info == null) {
128                 // If the tag is not in the schema map let spring handle the value by forwarding the tag as the
129                 // propertyName
130                 propertyName = tag;
131                 type = findBeanType(children.get(i));
132 
133             } else {
134                 // If the tag is found in the schema map use the connected name stored in the attribute information
135                 propertyName = info.getName();
136                 type = info.getType();
137             }
138             // Process the information stored in the child bean
139             ArrayList<Element> grandChildren = (ArrayList<Element>) DomUtils.getChildElements(children.get(i));
140 
141             if (type == BeanTagAttribute.AttributeType.SINGLEBEAN) {
142                 bean.addPropertyValue(propertyName, parseBean(grandChildren.get(0), bean, parserContext));
143             } else if (type == BeanTagAttribute.AttributeType.LISTBEAN) {
144                 bean.addPropertyValue(propertyName, parseList(grandChildren, children.get(i), bean, parserContext));
145             } else if (type == BeanTagAttribute.AttributeType.LISTVALUE) {
146                 bean.addPropertyValue(propertyName, parseList(grandChildren, children.get(i), bean, parserContext));
147             } else if (type == BeanTagAttribute.AttributeType.MAPVALUE) {
148                 bean.addPropertyValue(propertyName, parseMap(grandChildren, children.get(i), bean, parserContext));
149             } else if (type == BeanTagAttribute.AttributeType.MAPBEAN) {
150                 bean.addPropertyValue(propertyName, parseMap(grandChildren, children.get(i), bean, parserContext));
151             } else if (type == BeanTagAttribute.AttributeType.SETVALUE) {
152                 bean.addPropertyValue(propertyName, parseSet(grandChildren, children.get(i), bean, parserContext));
153             } else if (type == BeanTagAttribute.AttributeType.SETBEAN) {
154                 bean.addPropertyValue(propertyName, parseSet(grandChildren, children.get(i), bean, parserContext));
155             }
156         }
157         return;
158     }
159 
160     /**
161      * Adds the property value to the bean definition based on the name and value of the attribute.
162      *
163      * @param name - The name of the attribute.
164      * @param value - The value of the attribute.
165      * @param entries - The property entries for the over all tag.
166      * @param bean - The bean definition being created.
167      */
168     private void processSingleValue(String name, String value, Map<String, BeanTagAttributeInfo> entries,
169             BeanDefinitionBuilder bean) {
170 
171         if (name.toLowerCase().compareTo("parent") == 0) {
172             // If attribute is defining the parent set it in the bean builder.
173             bean.setParentName(value);
174         } else if (name.toLowerCase().compareTo("abstract") == 0) {
175             // If the attribute is defining the parent as  abstract set it in the bean builder.
176             bean.setAbstract(Boolean.valueOf(value));
177         } else if (name.toLowerCase().compareTo("id") == 0) {
178             if (value.contains("Demo-CollectionGrouping-Section1")) {
179                 System.out.println();
180             }
181 
182             //nothing - insures that its erased
183         } else {
184             // If the attribute is not a reserved case find the property name form the connected map and add the new
185             // property value.
186 
187             if (name.contains("-ref")) {
188                 bean.addPropertyValue(name.substring(0, name.length() - 4), new RuntimeBeanReference(value));
189             } else {
190                 BeanTagAttributeInfo info = entries.get(name);
191                 String propertyName;
192 
193                 if (info == null) {
194                     propertyName = name;
195                 } else {
196                     propertyName = info.getName();
197                 }
198                 bean.addPropertyValue(propertyName, value);
199             }
200         }
201     }
202 
203     /**
204      * Finds the key of a map entry in the custom schema.
205      *
206      * @param grandchild - The map entry.
207      * @return The object (bean or value) entry key
208      */
209     private Object findKey(Element grandchild) {
210         String key = grandchild.getAttribute("key");
211         if (!key.isEmpty()) {
212             return key;
213         } else {
214             Element keyTag = DomUtils.getChildElementByTagName(grandchild, "spring:key");
215             if (DomUtils.getChildElements(keyTag).size() == 0) {
216                 return keyTag.getTextContent();
217             } else {
218                 return DomUtils.getChildElements(keyTag).get(0);
219             }
220         }
221         //throw new Exception("Cannot find Map's key");
222     }
223 
224     /**
225      * Finds the value of a map entry in the custom schema.
226      *
227      * @param grandchild - The map entry.
228      * @return The object (bean or value) entry value
229      */
230     private Object findValue(Element grandchild) {
231         String value = grandchild.getAttribute("value");
232         if (!value.isEmpty()) {
233             return value;
234         } else {
235             Element valueTag = DomUtils.getChildElementByTagName(grandchild, "spring:value");
236             if (DomUtils.getChildElements(valueTag).size() == 0) {
237                 return valueTag.getTextContent();
238             } else {
239                 return DomUtils.getChildElements(valueTag).get(0);
240             }
241         }
242         //throw new Exception("Cannot find Map's value");
243     }
244 
245     /**
246      * Finds the attribute type of the schema being used by the element.
247      *
248      * @param tag - The tag to check.
249      * @return The schema attribute type.
250      */
251     private BeanTagAttribute.AttributeType findBeanType(Element tag) {
252         int numberChildren = 0;
253 
254         // Checks if the user overrides the default attribute type of the schema.
255         String overrideType = tag.getAttribute("overrideBeanType");
256         if (!StringUtils.isEmpty(overrideType)) {
257             if (overrideType.toLowerCase().compareTo("singlebean") == 0) {
258                 return BeanTagAttribute.AttributeType.SINGLEBEAN;
259             }
260             if (overrideType.toLowerCase().compareTo("singlevalue") == 0) {
261                 return BeanTagAttribute.AttributeType.SINGLEVALUE;
262             }
263             if (overrideType.toLowerCase().compareTo("listbean") == 0) {
264                 return BeanTagAttribute.AttributeType.LISTBEAN;
265             }
266             if (overrideType.toLowerCase().compareTo("listvalue") == 0) {
267                 return BeanTagAttribute.AttributeType.LISTVALUE;
268             }
269             if (overrideType.toLowerCase().compareTo("mapbean") == 0) {
270                 return BeanTagAttribute.AttributeType.MAPBEAN;
271             }
272             if (overrideType.toLowerCase().compareTo("mapvalue") == 0) {
273                 return BeanTagAttribute.AttributeType.MAPVALUE;
274             }
275             if (overrideType.toLowerCase().compareTo("setbean") == 0) {
276                 return BeanTagAttribute.AttributeType.SETBEAN;
277             }
278             if (overrideType.toLowerCase().compareTo("setvalue") == 0) {
279                 return BeanTagAttribute.AttributeType.SETVALUE;
280             }
281         }
282 
283         // Checks if the element is a list composed of standard types
284         numberChildren = DomUtils.getChildElementsByTagName(tag, "spring:value").size();
285         if (numberChildren > 0) {
286             return BeanTagAttribute.AttributeType.LISTVALUE;
287         }
288 
289         numberChildren = DomUtils.getChildElementsByTagName(tag, "spring:list").size();
290         if (numberChildren > 0) {
291             return BeanTagAttribute.AttributeType.LISTBEAN;
292         }
293 
294         numberChildren = DomUtils.getChildElementsByTagName(tag, "spring:set").size();
295         if (numberChildren > 0) {
296             return BeanTagAttribute.AttributeType.SETBEAN;
297         }
298 
299         // Checks if the element is a map
300         numberChildren = DomUtils.getChildElementsByTagName(tag, "spring:entry").size();
301         if (numberChildren > 0) {
302             return BeanTagAttribute.AttributeType.MAPVALUE;
303         }
304 
305         numberChildren = DomUtils.getChildElementsByTagName(tag, "spring:map").size();
306         if (numberChildren > 0) {
307             return BeanTagAttribute.AttributeType.MAPBEAN;
308         }
309 
310         // Checks if the element is a list of beans
311         numberChildren = DomUtils.getChildElements(tag).size();
312         if (numberChildren > 1) {
313             return BeanTagAttribute.AttributeType.LISTBEAN;
314         }
315 
316         // Defaults to return the element as a single bean.
317         return BeanTagAttribute.AttributeType.SINGLEBEAN;
318     }
319 
320     /**
321      * Parses a bean based on the namespace of the bean.
322      *
323      * @param tag - The Element to be parsed.
324      * @param parent - The parent bean that the tag is nested in.
325      * @param parserContext - Provided information and functionality regarding current bean set.
326      * @return The parsed bean.
327      */
328     private Object parseBean(Element tag, BeanDefinitionBuilder parent, ParserContext parserContext) {
329         if (tag.getNamespaceURI().compareTo("http://www.springframework.org/schema/beans") == 0) {
330             return parseSpringBean(tag, parserContext);
331         } else {
332             return parseCustomBean(tag, parent, parserContext);
333         }
334     }
335 
336     /**
337      * Parses a bean of the spring namespace.
338      *
339      * @param tag - The Element to be parsed.
340      * @return The parsed bean.
341      */
342     private Object parseSpringBean(Element tag, ParserContext parserContext) {
343         if (tag.getLocalName().compareTo("ref") == 0) {
344             // Create the referenced bean by creating a new bean and setting its parent to the referenced bean
345             // then replace grand child with it
346             Element temp = tag.getOwnerDocument().createElement("bean");
347             temp.setAttribute("parent", tag.getAttribute("bean"));
348             tag = temp;
349             return new RuntimeBeanReference(tag.getAttribute("parent"));
350         }
351 
352         //peel off p: properties an make them actual property nodes - p-namespace does not work properly (unknown cause)
353         Document document = tag.getOwnerDocument();
354         NamedNodeMap attributes = tag.getAttributes();
355         for(int i = 0; i < attributes.getLength(); i++){
356             Node attribute = attributes.item(i);
357             String name = attribute.getNodeName();
358             if(name.startsWith("p:")){
359                 Element property = document.createElement("property");
360                 property.setAttribute("name", StringUtils.removeStart(name, "p:"));
361                 property.setAttribute("value", attribute.getTextContent());
362 
363                 if(tag.getFirstChild() != null){
364                     tag.insertBefore(property, tag.getFirstChild());
365                 }
366                 else{
367                     tag.appendChild(property);
368                 }
369             }
370         }
371 
372         // Create the bean definition for the grandchild and return it.
373         BeanDefinitionParserDelegate delegate = parserContext.getDelegate();
374         BeanDefinitionHolder bean = delegate.parseBeanDefinitionElement(tag);
375 
376         // Creates a custom name for the new bean.
377         String name = bean.getBeanDefinition().getParentName() + "$Customchild" + beanNumber;
378         if (tag.getAttribute("id") != null && !StringUtils.isEmpty(tag.getAttribute("id"))) {
379             name = tag.getAttribute("id");
380         } else {
381             beanNumber++;
382         }
383 
384         return new BeanDefinitionHolder(bean.getBeanDefinition(), name);
385     }
386 
387     /**
388      * Parses a bean of the custom namespace.
389      *
390      * @param tag - The Element to be parsed.
391      * @param parent - The parent bean that the tag is nested in.
392      * @param parserContext - Provided information and functionality regarding current bean set.
393      * @return The parsed bean.
394      */
395     private Object parseCustomBean(Element tag, BeanDefinitionBuilder parent, ParserContext parserContext) {
396         BeanDefinitionHolder bean;
397         if (tag.getLocalName().compareTo("ref") == 0) {
398             return new RuntimeBeanReference(tag.getAttribute("bean"));
399 
400         } else {
401             BeanDefinition beanDefinition = parserContext.getDelegate().parseCustomElement(tag,
402                     parent.getBeanDefinition());
403 
404             String name = beanDefinition.getParentName() + "$Customchild" + beanNumber;
405             if (tag.getAttribute("id") != null && !StringUtils.isEmpty(tag.getAttribute("id"))) {
406                 name = tag.getAttribute("id");
407             } else {
408                 beanNumber++;
409             }
410             bean = new BeanDefinitionHolder(beanDefinition, name);
411         }
412 
413         return bean;
414     }
415 
416     /**
417      * Parses a list of elements into a list of beans/standard content.
418      *
419      * @param grandChildren - The list of beans/content in a bean property
420      * @param child - The property tag for the parent.
421      * @param parent - The parent bean that the tag is nested in.
422      * @param parserContext - Provided information and functionality regarding current bean set.
423      * @return A managedList of the nested content.
424      */
425     private ManagedList parseList(ArrayList<Element> grandChildren, Element child, BeanDefinitionBuilder parent,
426             ParserContext parserContext) {
427         ArrayList<Object> listItems = new ArrayList<Object>();
428 
429         for (int i = 0; i < grandChildren.size(); i++) {
430             Element grandChild = grandChildren.get(i);
431 
432             if (grandChild.getTagName().compareTo("spring:value") == 0) {
433                 listItems.add(grandChild.getTextContent());
434             } else {
435                 listItems.add(parseBean(grandChild, parent, parserContext));
436             }
437         }
438 
439         String merge = child.getAttribute("merge");
440 
441         ManagedList beans = new ManagedList(listItems.size());
442 
443         if (merge != null) {
444             beans.setMergeEnabled(Boolean.valueOf(merge));
445         }
446 
447         beans.addAll(listItems);
448         return beans;
449     }
450 
451     /**
452      * Parses a list of elements into a set of beans/standard content.
453      *
454      * @param grandChildren - The set of beans/content in a bean property
455      * @param child - The property tag for the parent.
456      * @param parent - The parent bean that the tag is nested in.
457      * @param parserContext - Provided information and functionality regarding current bean set.
458      * @return A managedSet of the nested content.
459      */
460     private ManagedSet parseSet(ArrayList<Element> grandChildren, Element child, BeanDefinitionBuilder parent,
461             ParserContext parserContext) {
462         ManagedSet setItems = new ManagedSet();
463 
464         for (int i = 0; i < grandChildren.size(); i++) {
465             Element grandChild = grandChildren.get(i);
466 
467             if (child.getTagName().compareTo("spring:value") == 0) {
468                 setItems.add(grandChild.getTextContent());
469             } else {
470                 setItems.add(parseBean(grandChild, parent, parserContext));
471             }
472         }
473 
474         String merge = child.getAttribute("merge");
475         if (merge != null) {
476             setItems.setMergeEnabled(Boolean.valueOf(merge));
477         }
478 
479         return setItems;
480     }
481 
482     /**
483      * Parses a list of elements into a map of beans/standard content.
484      *
485      * @param grandChildren - The list of beans/content in a bean property
486      * @param child - The property tag for the parent.
487      * @param parent - The parent bean that the tag is nested in.
488      * @param parserContext - Provided information and functionality regarding current bean set.
489      * @return A managedSet of the nested content.
490      */
491     private ManagedMap parseMap(ArrayList<Element> grandChildren, Element child, BeanDefinitionBuilder parent,
492             ParserContext parserContext) {
493         ManagedMap map = new ManagedMap();
494         String merge = child.getAttribute("merge");
495 
496         for (int j = 0; j < grandChildren.size(); j++) {
497             Object key = findKey(grandChildren.get(j));
498             Object value = findValue(grandChildren.get(j));
499             map.put(key, value);
500         }
501 
502         if (merge != null) {
503             map.setMergeEnabled(Boolean.valueOf(merge));
504         }
505 
506         return map;
507     }
508 }