1 /*
2 * Copyright 2010-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License").
5 * You may not use this file except in compliance with the License.
6 * A copy of the License is located at
7 *
8 * http://aws.amazon.com/apache2.0
9 *
10 * or in the "license" file accompanying this file. This file is distributed
11 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12 * express or implied. See the License for the specific language governing
13 * permissions and limitations under the License.
14 */
15 package com.amazonaws.transform;
16
17 import java.util.ArrayList;
18 import java.util.HashMap;
19 import java.util.Iterator;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.Stack;
23
24 import javax.xml.stream.XMLEventReader;
25 import javax.xml.stream.XMLStreamConstants;
26 import javax.xml.stream.XMLStreamException;
27 import javax.xml.stream.events.Attribute;
28 import javax.xml.stream.events.XMLEvent;
29
30 /**
31 * Contains the unmarshalling state for the parsing of an XML response. The
32 * unmarshallers are stateless so that they can be reused, so this class holds
33 * the state while different unmarshallers work together to parse an XML
34 * response. It also tracks the current position and element depth of the
35 * document being parsed and provides utilties for accessing the next XML event
36 * from the parser, reading element text, handling attribute XML events, etc.
37 */
38 public class StaxUnmarshallerContext {
39
40 private XMLEvent currentEvent;
41 private final XMLEventReader eventReader;
42
43 public final Stack<String> stack = new Stack<String>();
44 private String stackString = "";
45
46 private Map<String, String> metadata = new HashMap<String, String>();
47 private List<MetadataExpression> metadataExpressions = new ArrayList<MetadataExpression>();
48
49 private Iterator<?> attributeIterator;
50 private final Map<String, String> headers;
51
52 private String currentHeader;
53
54 public void setCurrentHeader(String currentHeader) {
55 this.currentHeader = currentHeader;
56 }
57
58 public boolean isInsideResponseHeader() {
59 return currentEvent == null;
60 }
61
62 /**
63 * Constructs a new unmarshaller context using the specified source of XML events.
64 *
65 * @param eventReader
66 * The source of XML events for this unmarshalling context.
67 */
68 public StaxUnmarshallerContext(XMLEventReader eventReader) {
69 this(eventReader, null);
70 }
71
72 /**
73 * Constructs a new unmarshaller context using the specified source of XML
74 * events, and a set of response headers.
75 *
76 * @param eventReader
77 * The source of XML events for this unmarshalling context.
78 * @param headers
79 * The set of response headers associated with this unmarshaller
80 * context.
81 */
82 public StaxUnmarshallerContext(XMLEventReader eventReader, Map<String, String> headers) {
83 this.eventReader = eventReader;
84 this.headers = headers;
85 }
86
87 /**
88 * Returns the value of the header with the specified name from the
89 * response, or null if not present.
90 *
91 * @param header
92 * The name of the header to lookup.
93 *
94 * @return The value of the header with the specified name from the
95 * response, or null if not present.
96 */
97 public String getHeader(String header) {
98 if (headers == null) return null;
99
100 return headers.get(header);
101 }
102
103 /**
104 * Returns the text contents of the current element being parsed.
105 *
106 * @return The text contents of the current element being parsed.
107 * @throws XMLStreamException
108 */
109 public String readText() throws XMLStreamException {
110 if (isInsideResponseHeader()) {
111 return getHeader(currentHeader);
112 }
113 if (currentEvent.isAttribute()) {
114 Attribute attribute = (Attribute)currentEvent;
115 return attribute.getValue();
116 }
117
118 StringBuilder sb = new StringBuilder();
119 while (true) {
120 XMLEvent event = eventReader.peek();
121 if (event.getEventType() == XMLStreamConstants.CHARACTERS) {
122 eventReader.nextEvent();
123 sb.append(event.asCharacters().getData());
124 } else if (event.getEventType() == XMLStreamConstants.END_ELEMENT) {
125 return sb.toString();
126 } else {
127 throw new RuntimeException("Encountered unexpected event: " + event.toString());
128 }
129 }
130 }
131
132 /**
133 * Returns the element depth of the parser's current position in the XML
134 * document being parsed.
135 *
136 * @return The element depth of the parser's current position in the XML
137 * document being parsed.
138 */
139 public int getCurrentDepth() {
140 return stack.size();
141 }
142
143 /**
144 * Tests the specified expression against the current position in the XML
145 * document being parsed.
146 *
147 * @param expression
148 * The psuedo-xpath expression to test.
149 * @return True if the expression matches the current document position,
150 * otherwise false.
151 */
152 public boolean testExpression(String expression) {
153 if (expression.equals(".")) return true;
154 return stackString.endsWith(expression);
155 }
156
157 /**
158 * Tests the specified expression against the current position in the XML
159 * document being parsed, and restricts the expression to matching at the
160 * specified stack depth.
161 *
162 * @param expression
163 * The psuedo-xpath expression to test.
164 * @param startingStackDepth
165 * The depth in the stack representing where the expression must
166 * start matching in order for this method to return true.
167 *
168 * @return True if the specified expression matches the current position in
169 * the XML document, starting from the specified depth.
170 */
171 public boolean testExpression(String expression, int startingStackDepth) {
172 if (expression.equals(".")) return true;
173
174 int index = -1;
175 while ((index = expression.indexOf("/", index + 1)) > -1) {
176 // Don't consider attributes a new depth level
177 if (expression.charAt(index + 1) != '@') {
178 startingStackDepth++;
179 }
180 }
181
182
183 return (startingStackDepth == getCurrentDepth()
184 && stackString.endsWith("/" + expression));
185 }
186
187 /**
188 * Returns true if this unmarshaller context is at the very beginning of a
189 * source document (i.e. no data has been parsed from the document yet).
190 *
191 * @return true if this unmarshaller context is at the very beginning of a
192 * source document (i.e. no data has been parsed from the document
193 * yet).
194 */
195 public boolean isStartOfDocument() throws XMLStreamException {
196 return eventReader.peek().isStartDocument();
197 }
198
199 /**
200 * Returns the next XML event for the document being parsed.
201 *
202 * @return The next XML event for the document being parsed.
203 *
204 * @throws XMLStreamException
205 */
206 public XMLEvent nextEvent() throws XMLStreamException {
207 if (attributeIterator != null && attributeIterator.hasNext()) {
208 currentEvent = (XMLEvent)attributeIterator.next();
209 } else {
210 currentEvent = eventReader.nextEvent();
211 }
212
213 if (currentEvent.isStartElement()) {
214 attributeIterator = currentEvent.asStartElement().getAttributes();
215 }
216
217 updateContext(currentEvent);
218
219 if (eventReader.hasNext()) {
220 XMLEvent nextEvent = eventReader.peek();
221 if (nextEvent != null && nextEvent.isCharacters()) {
222 for (MetadataExpression metadataExpression : metadataExpressions) {
223 if (testExpression(metadataExpression.expression, metadataExpression.targetDepth)) {
224 metadata.put(metadataExpression.key, nextEvent.asCharacters().getData());
225 }
226 }
227 }
228 }
229
230 return currentEvent;
231 }
232
233 /**
234 * Returns any metadata collected through metadata expressions while this
235 * context was reading the XML events from the XML document.
236 *
237 * @return A map of any metadata collected through metadata expressions
238 * while this context was reading the XML document.
239 */
240 public Map<String, String> getMetadata() {
241 return metadata;
242 }
243
244 /**
245 * Registers an expression, which if matched, will cause the data for the
246 * matching element to be stored in the metadata map under the specified
247 * key.
248 *
249 * @param expression
250 * The expression an element must match in order for it's data to
251 * be pulled out and stored in the metadata map.
252 * @param targetDepth
253 * The depth in the XML document where the expression match must
254 * start.
255 * @param storageKey
256 * The key under which to store the matching element's data.
257 */
258 public void registerMetadataExpression(String expression, int targetDepth, String storageKey) {
259 metadataExpressions.add(new MetadataExpression(expression, targetDepth, storageKey));
260 }
261
262
263 /*
264 * Private Interface
265 */
266
267 /**
268 * Simple container for the details of a metadata expression this
269 * unmarshaller context is looking for.
270 */
271 private static class MetadataExpression {
272 public String expression;
273 public int targetDepth;
274 public String key;
275
276 public MetadataExpression(String expression, int targetDepth, String key) {
277 this.expression = expression;
278 this.targetDepth = targetDepth;
279 this.key = key;
280 }
281 }
282
283 private void updateContext(XMLEvent event) {
284 if (event == null) return;
285
286 if (event.isEndElement()) {
287 stack.pop();
288 stackString = "";
289 for (String s : stack) {
290 stackString += "/" + s;
291 }
292 } else if (event.isStartElement()) {
293 stack.push(event.asStartElement().getName().getLocalPart());
294 stackString += "/" + event.asStartElement().getName().getLocalPart();
295 } else if (event.isAttribute()) {
296 Attribute attribute = (Attribute)event;
297 stackString = "";
298 for (String s : stack) {
299 stackString += "/" + s;
300 }
301 stackString += "/@" + attribute.getName().getLocalPart();
302 }
303 }
304
305 }
306