001    /**
002     * Copyright 2005-2012 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     */
016    package org.kuali.rice.kew.dto;
017    
018    import com.google.common.base.Functions;
019    import com.google.common.base.Joiner;
020    import com.google.common.collect.Iterables;
021    import org.apache.commons.lang.StringUtils;
022    import org.apache.log4j.Logger;
023    import org.kuali.rice.core.api.exception.RiceRuntimeException;
024    import org.kuali.rice.core.api.reflect.DataDefinition;
025    import org.kuali.rice.core.api.reflect.ObjectDefinition;
026    import org.kuali.rice.core.api.reflect.PropertyDefinition;
027    import org.kuali.rice.core.api.resourceloader.GlobalResourceLoader;
028    import org.kuali.rice.core.api.uif.RemotableAttributeErrorContract;
029    import org.kuali.rice.core.api.util.xml.XmlHelper;
030    import org.kuali.rice.core.api.util.xml.XmlJotter;
031    import org.kuali.rice.kew.actionrequest.ActionRequestValue;
032    import org.kuali.rice.kew.actiontaken.ActionTakenValue;
033    import org.kuali.rice.kew.api.KewApiServiceLocator;
034    import org.kuali.rice.kew.api.WorkflowRuntimeException;
035    import org.kuali.rice.kew.api.action.ActionRequest;
036    import org.kuali.rice.kew.api.action.ActionTaken;
037    import org.kuali.rice.kew.api.document.DocumentContentUpdate;
038    import org.kuali.rice.kew.api.document.DocumentDetail;
039    import org.kuali.rice.kew.api.document.InvalidDocumentContentException;
040    import org.kuali.rice.kew.api.document.attribute.WorkflowAttributeDefinition;
041    import org.kuali.rice.kew.api.extension.ExtensionDefinition;
042    import org.kuali.rice.kew.api.extension.ExtensionUtils;
043    import org.kuali.rice.kew.definition.AttributeDefinition;
044    import org.kuali.rice.kew.engine.node.RouteNodeInstance;
045    import org.kuali.rice.kew.framework.document.attribute.SearchableAttribute;
046    import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
047    import org.kuali.rice.kew.routeheader.StandardDocumentContent;
048    import org.kuali.rice.kew.rule.WorkflowRuleAttribute;
049    import org.kuali.rice.kew.rule.WorkflowAttributeXmlValidator;
050    import org.kuali.rice.kew.rule.XmlConfiguredAttribute;
051    import org.kuali.rice.kew.rule.bo.RuleAttribute;
052    import org.kuali.rice.kew.rule.xmlrouting.GenericXMLRuleAttribute;
053    import org.kuali.rice.kew.service.KEWServiceLocator;
054    import org.kuali.rice.kew.api.KewApiConstants;
055    import org.w3c.dom.Document;
056    import org.w3c.dom.Element;
057    import org.w3c.dom.NodeList;
058    import org.xml.sax.SAXException;
059    
060    import javax.xml.namespace.QName;
061    import javax.xml.parsers.DocumentBuilder;
062    import javax.xml.parsers.DocumentBuilderFactory;
063    import javax.xml.parsers.ParserConfigurationException;
064    import javax.xml.transform.TransformerException;
065    import java.io.IOException;
066    import java.util.ArrayList;
067    import java.util.HashMap;
068    import java.util.Iterator;
069    import java.util.List;
070    import java.util.Map;
071    
072    /**
073     * Translates Workflow server side beans into client side VO beans.
074     *
075     * @author Kuali Rice Team (rice.collab@kuali.org)
076     */
077    public class DTOConverter {
078        private static final Logger LOG = Logger.getLogger(DTOConverter.class);
079    
080        public static String buildUpdatedDocumentContent(String existingDocContent,
081                DocumentContentUpdate documentContentUpdate, String documentTypeName) {
082            if (existingDocContent == null) {
083                existingDocContent = KewApiConstants.DEFAULT_DOCUMENT_CONTENT;
084            }
085            String documentContent = KewApiConstants.DEFAULT_DOCUMENT_CONTENT;
086            StandardDocumentContent standardDocContent = new StandardDocumentContent(existingDocContent);
087            try {
088                DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
089                Document document = builder.newDocument();
090                Element root = document.createElement(KewApiConstants.DOCUMENT_CONTENT_ELEMENT);
091                document.appendChild(root);
092                Element applicationContentElement = standardDocContent.getApplicationContent();
093                if (documentContentUpdate.getApplicationContent() != null) {
094                    // application content has changed
095                    if (!StringUtils.isEmpty(documentContentUpdate.getApplicationContent())) {
096                        applicationContentElement = document.createElement(KewApiConstants.APPLICATION_CONTENT_ELEMENT);
097                        XmlHelper.appendXml(applicationContentElement, documentContentUpdate.getApplicationContent());
098                    } else {
099                        // they've cleared the application content
100                        applicationContentElement = null;
101                    }
102                }
103                Element attributeContentElement = createDocumentContentSection(document,
104                        standardDocContent.getAttributeContent(), documentContentUpdate.getAttributeDefinitions(),
105                        documentContentUpdate.getAttributeContent(), KewApiConstants.ATTRIBUTE_CONTENT_ELEMENT,
106                        documentTypeName);
107                Element searchableContentElement = createDocumentContentSection(document,
108                        standardDocContent.getSearchableContent(), documentContentUpdate.getSearchableDefinitions(),
109                        documentContentUpdate.getSearchableContent(), KewApiConstants.SEARCHABLE_CONTENT_ELEMENT,
110                        documentTypeName);
111                if (applicationContentElement != null) {
112                    root.appendChild(applicationContentElement);
113                }
114                if (attributeContentElement != null) {
115                    root.appendChild(attributeContentElement);
116                }
117                if (searchableContentElement != null) {
118                    root.appendChild(searchableContentElement);
119                }
120                documentContent = XmlJotter.jotNode(document);
121            } catch (ParserConfigurationException e) {
122                throw new RiceRuntimeException("Failed to initialize XML parser.", e);
123            } catch (SAXException e) {
124                throw new InvalidDocumentContentException("Failed to parse XML.", e);
125            } catch (IOException e) {
126                throw new InvalidDocumentContentException("Failed to parse XML.", e);
127            } catch (TransformerException e) {
128                throw new InvalidDocumentContentException("Failed to parse XML.", e);
129            }
130            return documentContent;
131        }
132    
133        private static Element createDocumentContentSection(Document document, Element existingAttributeElement,
134                List<WorkflowAttributeDefinition> definitions, String content, String elementName,
135                String documentTypeName) throws TransformerException, SAXException, IOException, ParserConfigurationException {
136            Element contentSectionElement = existingAttributeElement;
137            // if they've updated the content, we're going to re-build the content section element from scratch
138            if (content != null) {
139                if (!org.apache.commons.lang.StringUtils.isEmpty(content)) {
140                    contentSectionElement = document.createElement(elementName);
141                    // if they didn't merely clear the content, let's build the content section element by combining the children
142                    // of the incoming XML content
143                    Element incomingAttributeElement = XmlHelper.readXml(content).getDocumentElement();
144                    NodeList children = incomingAttributeElement.getChildNodes();
145                    for (int index = 0; index < children.getLength(); index++) {
146                        contentSectionElement.appendChild(document.importNode(children.item(index), true));
147                    }
148                } else {
149                    contentSectionElement = null;
150                }
151            }
152            // if they have new definitions we're going to append those to the existing content section
153            if (definitions != null && !definitions.isEmpty()) {
154                String errorMessage = "";
155                boolean inError = false;
156                if (contentSectionElement == null) {
157                    contentSectionElement = document.createElement(elementName);
158                }
159                for (WorkflowAttributeDefinition definitionVO : definitions) {
160                    AttributeDefinition definition = convertWorkflowAttributeDefinition(definitionVO);
161                    ExtensionDefinition extensionDefinition = definition.getExtensionDefinition();
162    
163                    Object attribute = null;
164                    attribute = GlobalResourceLoader.getObject(definition.getObjectDefinition());
165                    if (attribute == null) {
166                        attribute = GlobalResourceLoader.getService(QName.valueOf(
167                                definition.getExtensionDefinition().getResourceDescriptor()));
168                    }
169    
170                    if (attribute instanceof XmlConfiguredAttribute) {
171                        ((XmlConfiguredAttribute)attribute).setExtensionDefinition(RuleAttribute.to(definition.getRuleAttribute()));
172                    }
173                    boolean propertiesAsMap = false;
174                    if (KewApiConstants.RULE_XML_ATTRIBUTE_TYPE.equals(extensionDefinition.getType())) {
175                        propertiesAsMap = true;
176                    }
177                    if (propertiesAsMap) {
178                        for (org.kuali.rice.kew.api.document.PropertyDefinition propertyDefinitionVO : definitionVO
179                                .getPropertyDefinitions()) {
180                            if (attribute instanceof GenericXMLRuleAttribute) {
181                                ((GenericXMLRuleAttribute) attribute).getParamMap().put(propertyDefinitionVO.getName(),
182                                        propertyDefinitionVO.getValue());
183                            }
184                        }
185                    }
186    
187                    // validate inputs from client application if the attribute is capable
188                    if (attribute instanceof WorkflowAttributeXmlValidator) {
189                        List<? extends RemotableAttributeErrorContract> errors =
190                                ((WorkflowAttributeXmlValidator) attribute).validateClientRoutingData();
191                        if (!errors.isEmpty()) {
192                            inError = true;
193                            errorMessage += "Error validating attribute " + definitionVO.getAttributeName() + " ";
194                            errorMessage += Joiner.on("; ").join(Iterables.transform(errors, Functions.toStringFunction()));
195                        }
196                    }
197                    // dont add to xml if attribute is in error
198                    if (!inError) {
199                        if (attribute instanceof WorkflowRuleAttribute) {
200                            String attributeDocContent = ((WorkflowRuleAttribute) attribute).getDocContent();
201                            if (!StringUtils.isEmpty(attributeDocContent)) {
202                                XmlHelper.appendXml(contentSectionElement, attributeDocContent);
203                            }
204                        } else if (attribute instanceof SearchableAttribute) {
205                            SearchableAttribute searchableAttribute = (SearchableAttribute) attribute;
206                            String searchableAttributeContent = searchableAttribute.generateSearchContent(extensionDefinition, documentTypeName,
207                                    definitionVO);
208                            if (!StringUtils.isBlank(searchableAttributeContent)) {
209                                XmlHelper.appendXml(contentSectionElement, searchableAttributeContent);
210                            }
211                        }
212                    }
213                }
214                if (inError) {
215                    throw new WorkflowRuntimeException(errorMessage);
216                }
217    
218            }
219            if (contentSectionElement != null) {
220                // always be sure and import the element into the new document, if it originated from the existing doc content
221                // and
222                // appended to it, it will need to be imported
223                contentSectionElement = (Element) document.importNode(contentSectionElement, true);
224            }
225            return contentSectionElement;
226        }
227    
228        /**
229         * New for Rice 2.0
230         */
231        public static AttributeDefinition convertWorkflowAttributeDefinition(WorkflowAttributeDefinition definition) {
232            if (definition == null) {
233                return null;
234            }
235    
236            ExtensionDefinition extensionDefinition = KewApiServiceLocator.getExtensionRepositoryService().getExtensionByName(definition.getAttributeName());
237            if (extensionDefinition == null) {
238                throw new WorkflowRuntimeException("Extension " + definition.getAttributeName() + " not found");
239            }
240            RuleAttribute ruleAttribute = KEWServiceLocator.getRuleAttributeService().findByName(definition.getAttributeName());
241            if (ruleAttribute == null) {
242                throw new WorkflowRuntimeException("Attribute " + definition.getAttributeName() + " not found");
243            }
244    
245            ObjectDefinition objectDefinition = new ObjectDefinition(extensionDefinition.getResourceDescriptor());
246            if (definition.getParameters() != null) {
247                for (String parameter : definition.getParameters()) {
248                    objectDefinition.addConstructorParameter(new DataDefinition(parameter, String.class));
249                }
250            }
251            boolean propertiesAsMap = KewApiConstants.RULE_XML_ATTRIBUTE_TYPE.equals(extensionDefinition.getType()) || KewApiConstants
252                    .SEARCHABLE_XML_ATTRIBUTE_TYPE.equals(extensionDefinition.getType());
253            if (!propertiesAsMap && definition.getPropertyDefinitions() != null) {
254                for (org.kuali.rice.kew.api.document.PropertyDefinition propertyDefinition : definition
255                        .getPropertyDefinitions()) {
256                    objectDefinition.addProperty(new PropertyDefinition(propertyDefinition.getName(), new DataDefinition(
257                            propertyDefinition.getValue(), String.class)));
258                }
259            }
260    
261            return new AttributeDefinition(ruleAttribute, extensionDefinition, objectDefinition);
262        }
263    
264        /**
265         * Interface for a simple service providing RouteNodeInstanceS based on their IDs
266         */
267        public static interface RouteNodeInstanceLoader {
268            RouteNodeInstance load(String routeNodeInstanceID);
269        }
270    
271    
272        public static DocumentDetail convertDocumentDetailNew(DocumentRouteHeaderValue routeHeader) {
273            if (routeHeader == null) {
274                return null;
275            }
276            org.kuali.rice.kew.api.document.Document document = DocumentRouteHeaderValue.to(routeHeader);
277            DocumentDetail.Builder detail = DocumentDetail.Builder.create(document);
278            Map<String, RouteNodeInstance> nodeInstances = new HashMap<String, RouteNodeInstance>();
279            List<ActionRequest> actionRequestVOs = new ArrayList<ActionRequest>();
280            List<ActionRequestValue> rootActionRequests = KEWServiceLocator.getActionRequestService().getRootRequests(
281                    routeHeader.getActionRequests());
282            for (Iterator<ActionRequestValue> iterator = rootActionRequests.iterator(); iterator.hasNext(); ) {
283                ActionRequestValue actionRequest = iterator.next();
284                actionRequestVOs.add(ActionRequestValue.to(actionRequest));
285                RouteNodeInstance nodeInstance = actionRequest.getNodeInstance();
286                if (nodeInstance == null) {
287                    continue;
288                }
289                if (nodeInstance.getRouteNodeInstanceId() == null) {
290                    throw new IllegalStateException(
291                            "Error creating document detail structure because of NULL node instance id.");
292                }
293                nodeInstances.put(nodeInstance.getRouteNodeInstanceId(), nodeInstance);
294            }
295            detail.setActionRequests(actionRequestVOs);
296            List<org.kuali.rice.kew.api.document.node.RouteNodeInstance> nodeInstanceVOs =
297                    new ArrayList<org.kuali.rice.kew.api.document.node.RouteNodeInstance>();
298            for (Iterator<RouteNodeInstance> iterator = nodeInstances.values().iterator(); iterator.hasNext(); ) {
299                RouteNodeInstance nodeInstance = iterator.next();
300                nodeInstanceVOs.add(RouteNodeInstance.to(nodeInstance));
301            }
302            detail.setRouteNodeInstances(nodeInstanceVOs);
303            List<ActionTaken> actionTakenVOs = new ArrayList<ActionTaken>();
304            for (Object element : routeHeader.getActionsTaken()) {
305                ActionTakenValue actionTaken = (ActionTakenValue) element;
306                actionTakenVOs.add(ActionTakenValue.to(actionTaken));
307            }
308            detail.setActionsTaken(actionTakenVOs);
309            return detail.build();
310        }
311    
312    }