001/**
002 * Copyright 2005-2015 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.maintainablexml;
017
018import org.apache.commons.beanutils.PropertyUtils;
019import org.apache.commons.lang.StringUtils;
020import org.springframework.core.io.AbstractResource;
021import org.w3c.dom.Document;
022import org.w3c.dom.Element;
023import org.w3c.dom.Node;
024import org.w3c.dom.NodeList;
025import org.xml.sax.InputSource;
026
027import javax.xml.parsers.DocumentBuilder;
028import javax.xml.parsers.DocumentBuilderFactory;
029import javax.xml.transform.OutputKeys;
030import javax.xml.transform.Transformer;
031import javax.xml.transform.TransformerFactory;
032import javax.xml.transform.dom.DOMSource;
033import javax.xml.transform.stream.StreamResult;
034import javax.xml.xpath.XPath;
035import javax.xml.xpath.XPathConstants;
036import javax.xml.xpath.XPathExpression;
037import javax.xml.xpath.XPathExpressionException;
038import javax.xml.xpath.XPathFactory;
039import java.io.StringReader;
040import java.io.StringWriter;
041import java.lang.reflect.InvocationTargetException;
042import java.util.ArrayList;
043import java.util.Collection;
044import java.util.HashMap;
045import java.util.List;
046import java.util.Map;
047
048public class MaintainableXMLConversionServiceImpl {
049
050        private static final String SERIALIZATION_ATTRIBUTE = "serialization";
051        private static final String CLASS_ATTRIBUTE = "class";
052        private static final String MAINTENANCE_ACTION_ELEMENT_NAME = "maintenanceAction";
053    private static final String OLD_MAINTAINABLE_OBJECT_ELEMENT_NAME = "oldMaintainableObject";
054    private static final String NEW_MAINTAINABLE_OBJECT_ELEMENT_NAME = "newMaintainableObject";
055
056    private Map<String, String> classNameRuleMap;
057        private Map<String, Map<String, String>> classPropertyRuleMap;
058    private Map<String, String> dateRuleMap;
059        public MaintainableXMLConversionServiceImpl() {
060        }
061
062        public String transformMaintainableXML(String xml) {
063            String beginning = StringUtils.substringBefore(xml, "<" + OLD_MAINTAINABLE_OBJECT_ELEMENT_NAME + ">");
064        String oldMaintainableObjectXML = StringUtils.substringBetween(xml, "<" + OLD_MAINTAINABLE_OBJECT_ELEMENT_NAME + ">", "</" + OLD_MAINTAINABLE_OBJECT_ELEMENT_NAME + ">");
065        String newMaintainableObjectXML = StringUtils.substringBetween(xml, "<" + NEW_MAINTAINABLE_OBJECT_ELEMENT_NAME + ">", "</" + NEW_MAINTAINABLE_OBJECT_ELEMENT_NAME + ">");
066        String ending = StringUtils.substringAfter(xml, "</" + NEW_MAINTAINABLE_OBJECT_ELEMENT_NAME + ">");
067
068        String convertedOldMaintainableObjectXML = transformSection(oldMaintainableObjectXML);
069        String convertedNewMaintainableObjectXML = transformSection(newMaintainableObjectXML);
070
071        String convertedXML =  beginning +
072            "<" + OLD_MAINTAINABLE_OBJECT_ELEMENT_NAME + ">" + convertedOldMaintainableObjectXML +  "</" + OLD_MAINTAINABLE_OBJECT_ELEMENT_NAME + ">" +
073            "<" + NEW_MAINTAINABLE_OBJECT_ELEMENT_NAME + ">" + convertedNewMaintainableObjectXML +  "</" + NEW_MAINTAINABLE_OBJECT_ELEMENT_NAME + ">" +
074            ending;
075        return convertedXML;
076        }
077
078    private String transformSection(String xml) {
079
080        String maintenanceAction = StringUtils.substringBetween(xml, "<" + MAINTENANCE_ACTION_ELEMENT_NAME + ">", "</" + MAINTENANCE_ACTION_ELEMENT_NAME + ">");
081        xml = StringUtils.substringBefore(xml, "<" + MAINTENANCE_ACTION_ELEMENT_NAME + ">");
082
083        try {
084            xml = upgradeBONotes(xml);
085            if (classNameRuleMap == null) {
086                setRuleMaps();
087            }
088
089            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
090            DocumentBuilder db = dbf.newDocumentBuilder();
091            Document document = db.parse(new InputSource(new StringReader(xml)));
092
093            removePersonObjects(document);
094
095            for(Node childNode = document.getFirstChild(); childNode != null;) {
096                Node nextChild = childNode.getNextSibling();
097                transformClassNode(document, childNode);
098                childNode = nextChild;
099            }
100
101            TransformerFactory transFactory = TransformerFactory.newInstance();
102            Transformer trans = transFactory.newTransformer();
103            trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
104            trans.setOutputProperty(OutputKeys.INDENT, "yes");
105
106            StringWriter writer = new StringWriter();
107            StreamResult result = new StreamResult(writer);
108            DOMSource source = new DOMSource(document);
109            trans.transform(source, result);
110            xml = writer.toString().replaceAll("(?m)^\\s+\\n", "");
111        } catch (Exception e) {
112            e.printStackTrace();
113        }
114
115        return xml + "<" + MAINTENANCE_ACTION_ELEMENT_NAME + ">" + maintenanceAction + "</" + MAINTENANCE_ACTION_ELEMENT_NAME + ">";
116    }
117
118
119    /**
120     * Upgrades the old Bo notes tag that was part of the maintainable to the new notes tag.
121     *
122     * @param oldXML - the xml to upgrade
123     * @throws Exception
124     */
125    private String upgradeBONotes(String oldXML) throws Exception {
126        // Get the old bo note xml
127        String notesXml = StringUtils.substringBetween(oldXML, "<boNotes>", "</boNotes>");
128        if (notesXml != null) {
129            notesXml = notesXml.replace("org.kuali.rice.kns.bo.Note", "org.kuali.rice.krad.bo.Note");
130            notesXml = "<org.apache.ojb.broker.core.proxy.ListProxyDefaultImpl>\n"
131                    + notesXml
132                    + "\n</org.apache.ojb.broker.core.proxy.ListProxyDefaultImpl>";
133            oldXML = oldXML.replaceFirst(">", ">\n<notes>\n" + notesXml + "\n</notes>");
134        }
135        return oldXML;
136    }
137
138    public void removePersonObjects( Document doc ) {
139        XPath xpath = XPathFactory.newInstance().newXPath();
140        XPathExpression personProperties = null;
141        try {
142            personProperties = xpath.compile("//*[@class='org.kuali.rice.kim.impl.identity.PersonImpl']");
143            NodeList matchingNodes = (NodeList)personProperties.evaluate( doc, XPathConstants.NODESET );
144            for(int i = 0; i < matchingNodes.getLength(); i++) {
145                Node tempNode = matchingNodes.item(i);
146                tempNode.getParentNode().removeChild(tempNode);
147            }
148        } catch (XPathExpressionException e) {
149            e.printStackTrace();
150        }
151    }
152
153    private void transformClassNode(Document document, Node node) throws ClassNotFoundException, XPathExpressionException, IllegalAccessException, InvocationTargetException, NoSuchMethodException, InstantiationException {
154                String className = node.getNodeName();
155                if(this.classNameRuleMap.containsKey(className)) {
156                        String newClassName = this.classNameRuleMap.get(className);
157                        document.renameNode(node, null, newClassName);
158                        className = newClassName;
159                }
160            Class<?> dataObjectClass = Class.forName(className);
161                if(classPropertyRuleMap.containsKey(className)) {
162                        transformNode(document, node, dataObjectClass, classPropertyRuleMap.get(className));
163                }
164                transformNode(document, node, dataObjectClass, classPropertyRuleMap.get("*"));
165        }
166
167        private void transformNode(Document document, Node node, Class<?> currentClass, Map<String, String> propertyMappings) throws ClassNotFoundException, XPathExpressionException, IllegalAccessException, InvocationTargetException, NoSuchMethodException, InstantiationException {
168                for(Node childNode = node.getFirstChild(); childNode != null;) {
169                        Node nextChild = childNode.getNextSibling();
170                        String propertyName = childNode.getNodeName();
171                        if(childNode.hasAttributes()) {
172                                XPath xpath = XPathFactory.newInstance().newXPath();
173                                Node serializationAttribute = childNode.getAttributes().getNamedItem(SERIALIZATION_ATTRIBUTE);
174                                if(serializationAttribute != null && StringUtils.equals(serializationAttribute.getNodeValue(), "custom")) {
175                                        Node classAttribute = childNode.getAttributes().getNamedItem(CLASS_ATTRIBUTE);
176                                        if(classAttribute != null && StringUtils.equals(classAttribute.getNodeValue(), "org.kuali.rice.kns.util.TypedArrayList")) {
177                                                ((Element)childNode).removeAttribute(SERIALIZATION_ATTRIBUTE);
178                                                ((Element)childNode).removeAttribute(CLASS_ATTRIBUTE);
179                                                XPathExpression listSizeExpression = xpath.compile("//" + propertyName + "/org.apache.ojb.broker.core.proxy.ListProxyDefaultImpl/default/size/text()");
180                                                String size = (String)listSizeExpression.evaluate(childNode, XPathConstants.STRING);
181                                                List<Node> nodesToAdd = new ArrayList<Node>();
182                                                if(StringUtils.isNotBlank(size) && Integer.valueOf(size) > 0) {
183                                                        XPathExpression listTypeExpression = xpath.compile("//" + propertyName + "/org.kuali.rice.kns.util.TypedArrayList/default/listObjectType/text()");
184                                                        String listType = (String)listTypeExpression.evaluate(childNode, XPathConstants.STRING);
185                                                        XPathExpression listContentsExpression = xpath.compile("//" + propertyName + "/org.apache.ojb.broker.core.proxy.ListProxyDefaultImpl/" + listType);
186                                                        NodeList listContents = (NodeList)listContentsExpression.evaluate(childNode, XPathConstants.NODESET);
187                                                        for(int i = 0; i < listContents.getLength(); i++) {
188                                                                Node tempNode = listContents.item(i);
189                                                                transformClassNode(document, tempNode);
190                                                                nodesToAdd.add(tempNode);
191                                                        }
192                                                }
193                                                for(Node removeNode = childNode.getFirstChild(); removeNode != null;) {
194                                                        Node nextRemoveNode = removeNode.getNextSibling();
195                                                        childNode.removeChild(removeNode);
196                                                        removeNode = nextRemoveNode;
197                                                }
198                                                for(Node nodeToAdd : nodesToAdd) {
199                                                        childNode.appendChild(nodeToAdd);
200                                                }
201                                        } else {
202                                                ((Element)childNode).removeAttribute(SERIALIZATION_ATTRIBUTE);
203                                                
204                                                XPathExpression mapContentsExpression = xpath.compile("//" + propertyName + "/map/string");
205                                                NodeList mapContents = (NodeList)mapContentsExpression.evaluate(childNode, XPathConstants.NODESET);
206                                                List<Node> nodesToAdd = new ArrayList<Node>();
207                                                if(mapContents.getLength() > 0 && mapContents.getLength() % 2 == 0) {
208                                                        for(int i = 0; i < mapContents.getLength(); i++) {
209                                                                Node keyNode = mapContents.item(i);
210                                                                Node valueNode = mapContents.item(++i);
211                                                                Node entryNode = document.createElement("entry");
212                                                                entryNode.appendChild(keyNode);
213                                                                entryNode.appendChild(valueNode);
214                                                                nodesToAdd.add(entryNode);
215                                                        }
216                                                }
217                                                for(Node removeNode = childNode.getFirstChild(); removeNode != null;) {
218                                                        Node nextRemoveNode = removeNode.getNextSibling();
219                                                        childNode.removeChild(removeNode);
220                                                        removeNode = nextRemoveNode;
221                                                }
222                                                for(Node nodeToAdd : nodesToAdd) {
223                                                        childNode.appendChild(nodeToAdd);
224                                                }
225                                        }
226                                }
227                        }
228                        if(propertyMappings != null && propertyMappings.containsKey(propertyName)) {
229                                String newPropertyName = propertyMappings.get(propertyName);
230                                if(StringUtils.isNotBlank(newPropertyName)) {
231                                        document.renameNode(childNode, null, newPropertyName);
232                                        propertyName = newPropertyName;
233                                } else {
234                                        // If there is no replacement name then the element needs
235                                        // to be removed and skip all other processing
236                                        node.removeChild(childNode);
237                                        childNode = nextChild;
238                                        continue;
239                                }
240                        }
241
242            if(dateRuleMap != null && dateRuleMap.containsKey(propertyName)) {
243                String newDateValue = dateRuleMap.get(propertyName);
244                if(StringUtils.isNotBlank(newDateValue)) {
245                    if ( childNode.getTextContent().length() == 10 ) {
246                        childNode.setTextContent( childNode.getTextContent() + " " + newDateValue );
247
248                    }
249                }
250            }
251
252            if (currentClass != null) {
253                if (childNode.hasChildNodes() && !(Collection.class.isAssignableFrom(currentClass) || Map.class
254                        .isAssignableFrom(currentClass))) {
255                    Class<?> propertyClass = PropertyUtils.getPropertyType(currentClass.newInstance(), propertyName);
256                    if (propertyClass != null && classPropertyRuleMap.containsKey(propertyClass.getName())) {
257                        transformNode(document, childNode, propertyClass, this.classPropertyRuleMap.get(
258                                propertyClass.getName()));
259                    }
260                    transformNode(document, childNode, propertyClass, classPropertyRuleMap.get("*"));
261                }
262            }
263                        childNode = nextChild;
264                }
265        }
266
267    /**
268     * Reads the rule xml and sets up the rule maps that will be used to transform the xml
269     */
270        private void setRuleMaps() {
271                setupConfigurationMaps();
272                try {
273                        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
274                        DocumentBuilder db = dbf.newDocumentBuilder();
275                        
276                        AbstractResource resource = null;
277
278            Document doc = db.parse(getClass().getResourceAsStream(
279                    "/org/kuali/rice/devtools/krad/maintainablexml/MaintainableXMLUpgradeRules.xml"));
280
281                        doc.getDocumentElement().normalize();
282                        XPath xpath = XPathFactory.newInstance().newXPath();
283
284                        // Get the moved classes rules
285                        XPathExpression exprClassNames = xpath.compile("//*[@name='maint_doc_classname_changes']/pattern");
286                        NodeList classNamesList = (NodeList) exprClassNames.evaluate(doc, XPathConstants.NODESET);
287                        for (int s = 0; s < classNamesList.getLength(); s++) {
288                                String matchText = xpath.evaluate("match/text()", classNamesList.item(s));
289                                String replaceText = xpath.evaluate("replacement/text()", classNamesList.item(s));
290                                classNameRuleMap.put(matchText, replaceText);
291                        }
292
293                        // Get the property changed rules
294
295                        XPathExpression exprClassProperties = xpath.compile(
296                                        "//*[@name='maint_doc_changed_class_properties']/pattern");
297                        XPathExpression exprClassPropertiesPatterns = xpath.compile("pattern");
298                        NodeList propertyClassList = (NodeList) exprClassProperties.evaluate(doc, XPathConstants.NODESET);
299                        for (int s = 0; s < propertyClassList.getLength(); s++) {
300                                String classText = xpath.evaluate("class/text()", propertyClassList.item(s));
301                                Map<String, String> propertyRuleMap = new HashMap<String, String>();
302                                NodeList classPropertiesPatterns = (NodeList) exprClassPropertiesPatterns.evaluate(
303                                                propertyClassList.item(s), XPathConstants.NODESET);
304                                for (int c = 0; c < classPropertiesPatterns.getLength(); c++) {
305                                        String matchText = xpath.evaluate("match/text()", classPropertiesPatterns.item(c));
306                                        String replaceText = xpath.evaluate("replacement/text()", classPropertiesPatterns.item(c));
307                                        propertyRuleMap.put(matchText, replaceText);
308                                }
309                                classPropertyRuleMap.put(classText, propertyRuleMap);
310                        }
311
312            // Get the Date rules
313            XPathExpression dateFieldNames = xpath.compile("//*[@name='maint_doc_date_changes']/pattern");
314            NodeList DateNamesList = (NodeList) dateFieldNames.evaluate(doc, XPathConstants.NODESET);
315            for (int s = 0; s < DateNamesList.getLength(); s++) {
316                String matchText = xpath.evaluate("match/text()", DateNamesList.item(s));
317                String replaceText = xpath.evaluate("replacement/text()", DateNamesList.item(s));
318                dateRuleMap.put(matchText, replaceText);
319            }
320                } catch (Exception e) {
321                        System.out.println("Error parsing rule xml file. Please check file. : " + e.getMessage());
322                        e.printStackTrace();
323                }
324        }
325        
326        private void setupConfigurationMaps() {
327                classNameRuleMap = new HashMap<String, String>();
328                classPropertyRuleMap = new HashMap<String, Map<String,String>>();
329        dateRuleMap = new HashMap<String, String>();
330
331        // Pre-populate the class property rules with some defaults which apply to every BO
332                Map<String, String> defaultPropertyRules = new HashMap<String, String>();
333                defaultPropertyRules.put("boNotes", "");
334                defaultPropertyRules.put("autoIncrementSet", "");
335        classPropertyRuleMap.put("*", defaultPropertyRules);
336        }
337}