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}