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