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}