View Javadoc

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