1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.commons.scxml2.model;
18
19 import java.util.ArrayList;
20 import java.util.HashMap;
21 import java.util.List;
22 import java.util.Map;
23
24 import javax.xml.parsers.DocumentBuilderFactory;
25 import javax.xml.parsers.ParserConfigurationException;
26
27 import org.apache.commons.scxml2.Evaluator;
28 import org.w3c.dom.Document;
29 import org.w3c.dom.Element;
30 import org.w3c.dom.Node;
31 import org.w3c.dom.NodeList;
32
33 /**
34 * A <code>PayloadProvider</code> is an element in the SCXML document
35 * that can provide payload data for an event or an external process.
36 */
37 public abstract class PayloadProvider extends Action {
38
39 /**
40 * Payload data values wrapper list needed when multiple variable entries use the same names.
41 * The multiple values are then wrapped in a list. The PayloadBuilder uses this 'marker' list
42 * to distinguish between entry values which are a list themselves and the wrapper list.
43 */
44 private static class DataValueList extends ArrayList {
45 }
46
47 /**
48 * Adds an attribute and value to a payload data map.
49 * <p>
50 * As the SCXML specification allows for multiple payload attributes with the same name, this
51 * method takes care of merging multiple values for the same attribute in a list of values.
52 * </p>
53 * <p>
54 * Furthermore, as modifications of payload data on either the sender or receiver side should affect the
55 * the other side, attribute values (notably: {@link Node} value only for now) is cloned first before being added
56 * to the payload data map. This includes 'nested' values within a {@link NodeList}, {@link List} or {@link Map}.
57 * </p>
58 * @param attrName the name of the attribute to add
59 * @param attrValue the value of the attribute to add
60 * @param payload the payload data map to be updated
61 */
62 @SuppressWarnings("unchecked")
63 protected void addToPayload(final String attrName, final Object attrValue, Map<String, Object> payload) {
64 DataValueList valueList = null;
65 Object value = payload.get(attrName);
66 if (value != null) {
67 if (value instanceof DataValueList) {
68 valueList = (DataValueList)value;
69 }
70 else {
71 valueList = new DataValueList();
72 valueList.add(value);
73 payload.put(attrName, valueList);
74 }
75 }
76 value = clonePayloadValue(attrValue);
77 if (value instanceof List) {
78 if (valueList == null) {
79 valueList = new DataValueList();
80 payload.put(attrName, valueList);
81 }
82 valueList.addAll((List)value);
83 }
84 else if (valueList != null) {
85 valueList.add(value);
86 }
87 else {
88 payload.put(attrName, value);
89 }
90 }
91
92 /**
93 * Clones a value object for adding to a payload data map.
94 * <p>
95 * Currently only clones {@link Node} values.
96 * </p>
97 * <p>
98 * If the value object is an instanceof {@link NodeList}, {@link List} or {@link Map}, its elements
99 * 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 }