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}