• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 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 
16 package software.amazon.awssdk.protocols.xml.internal.marshall;
17 
18 import java.io.IOException;
19 import java.io.Writer;
20 import java.nio.ByteBuffer;
21 import java.util.Date;
22 import java.util.Map;
23 import java.util.Stack;
24 import software.amazon.awssdk.annotations.SdkInternalApi;
25 import software.amazon.awssdk.core.exception.SdkClientException;
26 import software.amazon.awssdk.utils.BinaryUtils;
27 import software.amazon.awssdk.utils.DateUtils;
28 import software.amazon.awssdk.utils.StringUtils;
29 
30 /**
31  * Utility for creating easily creating XML documents, one element at a time.
32  */
33 @SdkInternalApi
34 class XmlWriter {
35 
36     static final String[] ESCAPE_SEARCHES = {
37         // Ampersands should always be the first to escape
38         "&", "\"", "'", "<", ">", "\r", "\n"
39     };
40 
41     static final String[] ESCAPE_REPLACEMENTS = {
42         "&amp;", "&quot;", "&apos;", "&lt;", "&gt;", "&#x0D;", "&#x0A;"
43     };
44 
45     /** Standard XML prolog to add to the beginning of each XML document. */
46     private static final String PROLOG = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
47 
48     private static final String[] UNESCAPE_SEARCHES = {
49         // Ampersands should always be the last to unescape
50         "&quot;", "&apos;", "&lt;", "&gt;", "&#x0D;", "&#x0A;", "&amp;"
51     };
52 
53     private static final String[] UNESCAPE_REPLACEMENTS = {
54         "\"", "'", "<", ">", "\r", "\n", "&"
55     };
56 
57     /** The writer to which the XML document created by this writer will be written. */
58     private final Writer writer;
59 
60     /** Optional XML namespace attribute value to include in the root element. */
61     private final String xmlns;
62 
63     private Stack<String> elementStack = new Stack<>();
64     private boolean rootElement = true;
65     private boolean writtenProlog = false;
66 
67     /**
68      * Creates a new XMLWriter, ready to write an XML document to the specified
69      * writer. The root element in the XML document will specify an xmlns
70      * attribute with the specified namespace parameter.
71      *
72      * @param w
73      *            The writer this XMLWriter will write to.
74      * @param xmlns
75      *            The XML namespace to include in the xmlns attribute of the
76      *            root element.
77      */
XmlWriter(Writer w, String xmlns)78     XmlWriter(Writer w, String xmlns) {
79         this.writer = w;
80         this.xmlns = xmlns;
81     }
82 
83     /**
84      * Starts a new element with the specified name at the current position in
85      * the in-progress XML document.
86      *
87      * @param element
88      *            The name of the new element.
89      *
90      * @return This XMLWriter so that additional method calls can be chained
91      *         together.
92      */
startElement(String element)93     XmlWriter startElement(String element) {
94         // Only append the PROLOG if there is XML written.
95         if (!writtenProlog) {
96             writtenProlog = true;
97             append(PROLOG);
98         }
99         append("<" + element);
100         if (rootElement && xmlns != null) {
101             append(" xmlns=\"" + xmlns + "\"");
102             rootElement = false;
103         }
104         append(">");
105         elementStack.push(element);
106         return this;
107     }
108 
109     /**
110      * Start to write an element with xml attributes.
111      *
112      * @param element the elment to write
113      * @param attributes the xml attribtues
114      * @return the XmlWriter
115      */
startElement(String element, Map<String, String> attributes)116     XmlWriter startElement(String element, Map<String, String> attributes) {
117         append("<" + element);
118         for (Map.Entry<String, String> attribute: attributes.entrySet()) {
119             append(" " + attribute.getKey() + "=\"" + attribute.getValue() + "\"");
120         }
121         append(">");
122         elementStack.push(element);
123         return this;
124     }
125 
126     /**
127      * Closes the last opened element at the current position in the in-progress
128      * XML document.
129      *
130      * @return This XMLWriter so that additional method calls can be chained
131      *         together.
132      */
endElement()133     XmlWriter endElement() {
134         String lastElement = elementStack.pop();
135         append("</" + lastElement + ">");
136         return this;
137     }
138 
139     /**
140      * Adds the specified value as text to the current position of the in
141      * progress XML document.
142      *
143      * @param s
144      *            The text to add to the XML document.
145      *
146      * @return This XMLWriter so that additional method calls can be chained
147      *         together.
148      */
value(String s)149     public XmlWriter value(String s) {
150         append(escapeXmlEntities(s));
151         return this;
152     }
153 
154     /**
155      * Adds the specified value as Base64 encoded text to the current position of the in
156      * progress XML document.
157      *
158      * @param b
159      *            The binary data to add to the XML document.
160      *
161      * @return This XMLWriter so that additional method calls can be chained
162      *         together.
163      */
value(ByteBuffer b)164     public XmlWriter value(ByteBuffer b) {
165         append(escapeXmlEntities(BinaryUtils.toBase64(BinaryUtils.copyBytesFrom(b))));
166         return this;
167     }
168 
169     /**
170      * Adds the specified date as text to the current position of the
171      * in-progress XML document.
172      *
173      * @param date
174      *            The date to add to the XML document.
175      *
176      * @return This XMLWriter so that additional method calls can be chained
177      *         together.
178      */
value(Date date)179     public XmlWriter value(Date date) {
180         append(escapeXmlEntities(DateUtils.formatIso8601Date(date.toInstant())));
181         return this;
182     }
183 
184     /**
185      * Adds the string representation of the specified object to the current
186      * position of the in progress XML document.
187      *
188      * @param obj
189      *            The object to translate to a string and add to the XML
190      *            document.
191      *
192      * @return This XMLWriter so that additional method calls can be chained
193      *         together.
194      */
value(Object obj)195     public XmlWriter value(Object obj) {
196         append(escapeXmlEntities(obj.toString()));
197         return this;
198     }
199 
append(String s)200     private void append(String s) {
201         try {
202             writer.append(s);
203         } catch (IOException e) {
204             throw SdkClientException.builder().message("Unable to write XML document").cause(e).build();
205         }
206     }
207 
escapeXmlEntities(String s)208     protected String escapeXmlEntities(String s) {
209         // Unescape any escaped characters.
210         if (s.contains("&")) {
211             s = StringUtils.replaceEach(s, UNESCAPE_SEARCHES, UNESCAPE_REPLACEMENTS);
212         }
213         return StringUtils.replaceEach(s, ESCAPE_SEARCHES, ESCAPE_REPLACEMENTS);
214     }
215 }
216