001/**
002 * Copyright 2005-2016 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.kew.dto;
017
018import com.google.common.base.Functions;
019import com.google.common.base.Joiner;
020import com.google.common.collect.Iterables;
021import org.apache.commons.lang.StringUtils;
022import org.apache.log4j.Logger;
023import org.kuali.rice.core.api.exception.RiceRuntimeException;
024import org.kuali.rice.core.api.reflect.DataDefinition;
025import org.kuali.rice.core.api.reflect.ObjectDefinition;
026import org.kuali.rice.core.api.reflect.PropertyDefinition;
027import org.kuali.rice.core.api.resourceloader.GlobalResourceLoader;
028import org.kuali.rice.core.api.uif.RemotableAttributeErrorContract;
029import org.kuali.rice.core.api.util.xml.XmlHelper;
030import org.kuali.rice.core.api.util.xml.XmlJotter;
031import org.kuali.rice.kew.actionrequest.ActionRequestValue;
032import org.kuali.rice.kew.actiontaken.ActionTakenValue;
033import org.kuali.rice.kew.api.KewApiServiceLocator;
034import org.kuali.rice.kew.api.WorkflowRuntimeException;
035import org.kuali.rice.kew.api.action.ActionRequest;
036import org.kuali.rice.kew.api.action.ActionTaken;
037import org.kuali.rice.kew.api.document.DocumentContentUpdate;
038import org.kuali.rice.kew.api.document.DocumentDetail;
039import org.kuali.rice.kew.api.document.InvalidDocumentContentException;
040import org.kuali.rice.kew.api.document.attribute.WorkflowAttributeDefinition;
041import org.kuali.rice.kew.api.extension.ExtensionDefinition;
042import org.kuali.rice.kew.api.extension.ExtensionUtils;
043import org.kuali.rice.kew.definition.AttributeDefinition;
044import org.kuali.rice.kew.engine.node.RouteNodeInstance;
045import org.kuali.rice.kew.framework.document.attribute.SearchableAttribute;
046import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
047import org.kuali.rice.kew.routeheader.StandardDocumentContent;
048import org.kuali.rice.kew.rule.WorkflowRuleAttribute;
049import org.kuali.rice.kew.rule.WorkflowAttributeXmlValidator;
050import org.kuali.rice.kew.rule.XmlConfiguredAttribute;
051import org.kuali.rice.kew.rule.bo.RuleAttribute;
052import org.kuali.rice.kew.rule.xmlrouting.GenericXMLRuleAttribute;
053import org.kuali.rice.kew.service.KEWServiceLocator;
054import org.kuali.rice.kew.api.KewApiConstants;
055import org.w3c.dom.Document;
056import org.w3c.dom.Element;
057import org.w3c.dom.NodeList;
058import org.xml.sax.SAXException;
059
060import javax.xml.namespace.QName;
061import javax.xml.parsers.DocumentBuilder;
062import javax.xml.parsers.DocumentBuilderFactory;
063import javax.xml.parsers.ParserConfigurationException;
064import javax.xml.transform.TransformerException;
065import java.io.IOException;
066import java.util.ArrayList;
067import java.util.HashMap;
068import java.util.Iterator;
069import java.util.List;
070import 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 */
077public 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(definition.getExtensionDefinition());
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        //KULRICE-7643
236        ExtensionDefinition extensionDefinition = null;
237        List<RuleAttribute> ruleAttribute = KEWServiceLocator.getRuleAttributeService().findByClassName(definition.getAttributeName());
238        if (ruleAttribute == null || ruleAttribute.isEmpty()) {
239            extensionDefinition = KewApiServiceLocator.getExtensionRepositoryService().getExtensionByName(definition.getAttributeName());
240        }else{
241            //TODO: Should we do something more intelligent here? Rice 1.x returned only a single entry but we can now have a list
242            RuleAttribute tmpAttr = ruleAttribute.get(0);
243            extensionDefinition = RuleAttribute.to(tmpAttr);
244            if(ruleAttribute.size() > 1){
245                LOG.warn("AttributeDefinition lookup (findByClassName) returned multiple attribute for the same class name. This should not happen, investigation recommended for classname: " 
246            + definition.getAttributeName() + " which has " + ruleAttribute.size() + " entries.");
247            }
248        }
249        
250        if (extensionDefinition == null) {
251            throw new WorkflowRuntimeException("Extension " + definition.getAttributeName() + " not found");
252        }
253        /*RuleAttribute ruleAttribute = KEWServiceLocator.getRuleAttributeService().findByName(definition.getAttributeName());
254        if (ruleAttribute == null) {
255            throw new WorkflowRuntimeException("Attribute " + definition.getAttributeName() + " not found");
256        }*/
257
258        ObjectDefinition objectDefinition = new ObjectDefinition(extensionDefinition.getResourceDescriptor());
259        if (definition.getParameters() != null) {
260            for (String parameter : definition.getParameters()) {
261                objectDefinition.addConstructorParameter(new DataDefinition(parameter, String.class));
262            }
263        }
264        boolean propertiesAsMap = KewApiConstants.RULE_XML_ATTRIBUTE_TYPE.equals(extensionDefinition.getType()) || KewApiConstants
265                .SEARCHABLE_XML_ATTRIBUTE_TYPE.equals(extensionDefinition.getType());
266        if (!propertiesAsMap && definition.getPropertyDefinitions() != null) {
267            for (org.kuali.rice.kew.api.document.PropertyDefinition propertyDefinition : definition
268                    .getPropertyDefinitions()) {
269                objectDefinition.addProperty(new PropertyDefinition(propertyDefinition.getName(), new DataDefinition(
270                        propertyDefinition.getValue(), String.class)));
271            }
272        }
273
274        return new AttributeDefinition(extensionDefinition, objectDefinition);
275    }
276
277    /**
278     * Interface for a simple service providing RouteNodeInstanceS based on their IDs
279     */
280    public static interface RouteNodeInstanceLoader {
281        RouteNodeInstance load(String routeNodeInstanceID);
282    }
283
284
285    public static DocumentDetail convertDocumentDetailNew(DocumentRouteHeaderValue routeHeader) {
286        if (routeHeader == null) {
287            return null;
288        }
289        org.kuali.rice.kew.api.document.Document document = DocumentRouteHeaderValue.to(routeHeader);
290        DocumentDetail.Builder detail = DocumentDetail.Builder.create(document);
291        Map<String, RouteNodeInstance> nodeInstances = new HashMap<String, RouteNodeInstance>();
292        List<ActionRequest> actionRequestVOs = new ArrayList<ActionRequest>();
293        List<ActionRequestValue> rootActionRequests = KEWServiceLocator.getActionRequestService().getRootRequests(
294                routeHeader.getActionRequests());
295        for (Iterator<ActionRequestValue> iterator = rootActionRequests.iterator(); iterator.hasNext(); ) {
296            ActionRequestValue actionRequest = iterator.next();
297            actionRequestVOs.add(ActionRequestValue.to(actionRequest));
298            RouteNodeInstance nodeInstance = actionRequest.getNodeInstance();
299            if (nodeInstance == null) {
300                continue;
301            }
302            if (nodeInstance.getRouteNodeInstanceId() == null) {
303                throw new IllegalStateException(
304                        "Error creating document detail structure because of NULL node instance id.");
305            }
306            nodeInstances.put(nodeInstance.getRouteNodeInstanceId(), nodeInstance);
307        }
308        detail.setActionRequests(actionRequestVOs);
309        List<org.kuali.rice.kew.api.document.node.RouteNodeInstance> nodeInstanceVOs =
310                new ArrayList<org.kuali.rice.kew.api.document.node.RouteNodeInstance>();
311        for (Iterator<RouteNodeInstance> iterator = nodeInstances.values().iterator(); iterator.hasNext(); ) {
312            RouteNodeInstance nodeInstance = iterator.next();
313            nodeInstanceVOs.add(RouteNodeInstance.to(nodeInstance));
314        }
315        detail.setRouteNodeInstances(nodeInstanceVOs);
316        List<ActionTaken> actionTakenVOs = new ArrayList<ActionTaken>();
317        for (Object element : routeHeader.getActionsTaken()) {
318            ActionTakenValue actionTaken = (ActionTakenValue) element;
319            actionTakenVOs.add(ActionTakenValue.to(actionTaken));
320        }
321        detail.setActionsTaken(actionTakenVOs);
322        return detail.build();
323    }
324
325}