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 "&", """, "'", "<", ">", "
", "
" 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 """, "'", "<", ">", "
", "
", "&" 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