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 == nullreturn 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 == nullreturn;
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