View Javadoc
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 }