001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.scxml2.model;
018
019import java.util.ArrayList;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Map;
023
024import javax.xml.parsers.DocumentBuilderFactory;
025import javax.xml.parsers.ParserConfigurationException;
026
027import org.apache.commons.scxml2.Evaluator;
028import org.w3c.dom.Document;
029import org.w3c.dom.Element;
030import org.w3c.dom.Node;
031import org.w3c.dom.NodeList;
032
033/**
034 * A <code>PayloadProvider</code> is an element in the SCXML document
035 * that can provide payload data for an event or an external process.
036 */
037public abstract class PayloadProvider extends Action {
038
039    /**
040     * Payload data values wrapper list needed when multiple variable entries use the same names.
041     * The multiple values are then wrapped in a list. The PayloadBuilder uses this 'marker' list
042     * to distinguish between entry values which are a list themselves and the wrapper list.
043     */
044    private static class DataValueList extends ArrayList {
045    }
046
047    /**
048     * Adds an attribute and value to a payload data map.
049     * <p>
050     * As the SCXML specification allows for multiple payload attributes with the same name, this
051     * method takes care of merging multiple values for the same attribute in a list of values.
052     * </p>
053     * <p>
054     * Furthermore, as modifications of payload data on either the sender or receiver side should affect the
055     * the other side, attribute values (notably: {@link Node} value only for now) is cloned first before being added
056     * to the payload data map. This includes 'nested' values within a {@link NodeList}, {@link List} or {@link Map}.
057     * </p>
058     * @param attrName the name of the attribute to add
059     * @param attrValue the value of the attribute to add
060     * @param payload the payload data map to be updated
061     */
062    @SuppressWarnings("unchecked")
063    protected void addToPayload(final String attrName, final Object attrValue, Map<String, Object> payload) {
064        DataValueList valueList = null;
065        Object value = payload.get(attrName);
066        if (value != null) {
067            if (value instanceof DataValueList) {
068                valueList = (DataValueList)value;
069            }
070            else {
071                valueList = new DataValueList();
072                valueList.add(value);
073                payload.put(attrName, valueList);
074            }
075        }
076        value = clonePayloadValue(attrValue);
077        if (value instanceof List) {
078            if (valueList == null) {
079                valueList = new DataValueList();
080                payload.put(attrName, valueList);
081            }
082            valueList.addAll((List)value);
083        }
084        else if (valueList != null) {
085            valueList.add(value);
086        }
087        else {
088            payload.put(attrName, value);
089        }
090    }
091
092    /**
093     * Clones a value object for adding to a payload data map.
094     * <p>
095     * Currently only clones {@link Node} values.
096     * </p>
097     * <p>
098     * If the value object is an instanceof {@link NodeList}, {@link List} or {@link Map}, its elements
099     * are also cloned (if possible) through recursive invocation of this same method, and put in
100     * a new {@link List} or {@link Map} before returning.
101     * </p>
102     * @param value the value to be cloned
103     * @return the cloned value if it could be cloned or otherwise the unmodified value parameter
104     */
105    @SuppressWarnings("unchecked")
106    protected Object clonePayloadValue(final Object value) {
107        if (value != null) {
108            if (value instanceof Node) {
109                return ((Node)value).cloneNode(true);
110            }
111            else if (value instanceof NodeList) {
112                NodeList nodeList = (NodeList)value;
113                ArrayList<Node> list = new ArrayList<Node>();
114                for (int i = 0, size = nodeList.getLength(); i < size; i++) {
115                    list.add(nodeList.item(i).cloneNode(true));
116                }
117                return list;
118            }
119            else if (value instanceof List) {
120                ArrayList<Object> list = new ArrayList<Object>();
121                for (Object v : (List)value) {
122                    list.add(clonePayloadValue(v));
123                }
124                return list;
125            }
126            else if (value instanceof Map) {
127                HashMap<Object, Object> map = new HashMap<Object, Object>();
128                for (Map.Entry<Object,Object> entry : ((Map<Object,Object>)value).entrySet()) {
129                    map.put(entry.getKey(), clonePayloadValue(entry.getValue()));
130                }
131                return map;
132            }
133            // TODO: cloning other type of data?
134        }
135        return value;
136    }
137
138    /**
139     * Converts a payload data map to be used for an event payload.
140     * <p>
141     * Event payload involving key-value pair attributes for an xpath datamodel requires special handling as the
142     * attributes needs to be contained and put in a "data" element under a 'root' Event payload element.
143     * </p>
144     * <p>
145     * For non-xpath datamodels this method simply returns the original payload parameter unmodified.
146     * </p>
147     * @param evaluator the evaluator to test for which datamodel type this event payload is intended
148     * @param payload the payload data map
149     * @return payload for an event
150     * @throws ModelException
151     */
152    protected Object makeEventPayload(final Evaluator evaluator, final Map<String, Object> payload)
153            throws ModelException {
154        if (payload != null && !payload.isEmpty() && Evaluator.XPATH_DATA_MODEL.equals(evaluator.getSupportedDatamodel())) {
155
156            try {
157                Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
158                Element payloadNode = document.createElement("payload");
159                for (Map.Entry<String, Object> entry : payload.entrySet()) {
160                    Element dataNode = document.createElement("data");
161                    payloadNode.appendChild(dataNode);
162                    dataNode.setAttribute("id", entry.getKey());
163                    if (entry.getValue() instanceof Node) {
164                        dataNode.appendChild(document.importNode((Node)entry.getValue(), true));
165                    }
166                    else if (entry.getValue() instanceof DataValueList) {
167                        for (Object value : ((DataValueList)entry.getValue())) {
168                            if (value instanceof Node) {
169                                dataNode.appendChild(document.importNode((Node)entry.getValue(), true));
170                            }
171                            else {
172                                dataNode.setTextContent(String.valueOf(value));
173                            }
174                        }
175                    }
176                    else if (entry.getValue() != null) {
177                        dataNode.setTextContent(String.valueOf(entry.getValue()));
178                    }
179                }
180                return payloadNode;
181            }
182            catch (ParserConfigurationException pce) {
183                throw new ModelException("Cannot instantiate a DocumentBuilder", pce);
184            }
185        }
186        return payload;
187    }
188}