View Javadoc
1   /*
2     (C) Copyright IBM Corp. 2007, 2012
3   
4     THIS FILE IS PROVIDED UNDER THE TERMS OF THE ECLIPSE PUBLIC LICENSE
5     ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS FILE
6     CONSTITUTES RECIPIENTS ACCEPTANCE OF THE AGREEMENT.
7   
8     You can obtain a current copy of the Eclipse Public License from
9     http://www.opensource.org/licenses/eclipse-1.0.php
10  
11    @author : Alexander Wolf-Reber, IBM, a.wolf-reber@de.ibm.com
12   * 
13   * Change History
14   * Flag       Date        Prog         Description
15   *------------------------------------------------------------------------------- 
16   * 1671502    2007-02-08  lupusalex    Remove dependency from Xerces
17   * 2003590    2008-06-30  blaschke-oss Change licensing from CPL to EPL
18   * 2524131    2009-01-21  raman_arora  Upgrade client to JDK 1.5 (Phase 1)
19   * 2763216    2009-04-14  blaschke-oss Code cleanup: visible spelling/grammar errors
20   * 3510321    2012-03-23  blaschke-oss Handle CDATA in CimXmlSerializer
21   * 3513357    2012-04-01  blaschke-oss Handle multiple CDATAs in CimXmlSerializer
22   */
23  
24  package org.metricshub.wbem.sblim.cimclient.internal.cimxml;
25  
26  /*-
27   * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
28   * WBEM Java Client
29   * ჻჻჻჻჻჻
30   * Copyright 2023 - 2025 MetricsHub
31   * ჻჻჻჻჻჻
32   * Licensed under the Apache License, Version 2.0 (the "License");
33   * you may not use this file except in compliance with the License.
34   * You may obtain a copy of the License at
35   *
36   *      http://www.apache.org/licenses/LICENSE-2.0
37   *
38   * Unless required by applicable law or agreed to in writing, software
39   * distributed under the License is distributed on an "AS IS" BASIS,
40   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
41   * See the License for the specific language governing permissions and
42   * limitations under the License.
43   * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
44   */
45  
46  import java.io.BufferedWriter;
47  import java.io.IOException;
48  import java.io.OutputStream;
49  import java.io.OutputStreamWriter;
50  import java.nio.charset.Charset;
51  import org.metricshub.wbem.sblim.cimclient.internal.util.WBEMConstants;
52  import org.w3c.dom.Document;
53  import org.w3c.dom.NamedNodeMap;
54  import org.w3c.dom.Node;
55  
56  /**
57   * Class CimXmlSerializer implements a XML serializer for DOM documents that is
58   * specialized for CIM-XML. It might not be used as a general purpose serializer
59   * since it doesn't support any DOM or XML features not required by CIM-XML.
60   *
61   */
62  public class CimXmlSerializer {
63  
64  	/**
65  	 * Class XmlWriter implements a writer on an output stream that escapes XML
66  	 * values according to the CIM-XML requirements.
67  	 *
68  	 */
69  	private static class XmlWriter {
70  		private BufferedWriter iWriter;
71  
72  		/**
73  		 * Ctor.
74  		 *
75  		 * @param pOut
76  		 *            The output stream the serialized document is written to
77  		 * @param pCharsetName
78  		 *            The encoding the use for the output stream
79  		 */
80  		public XmlWriter(OutputStream pOut, String pCharsetName) {
81  			this.iWriter = new BufferedWriter(new OutputStreamWriter(pOut, Charset.forName(pCharsetName).newEncoder()));
82  		}
83  
84  		/**
85  		 * Writes text to the stream
86  		 *
87  		 * @param pText
88  		 *            The text
89  		 * @throws IOException
90  		 */
91  		public void write(String pText) throws IOException {
92  			if (pText != null) this.iWriter.write(pText);
93  		}
94  
95  		/**
96  		 * Closes the stream
97  		 *
98  		 * @throws IOException
99  		 */
100 		public void close() throws IOException {
101 			this.iWriter.close();
102 		}
103 
104 		/**
105 		 * Flushes the buffer to the stream
106 		 *
107 		 * @throws IOException
108 		 */
109 		public void flush() throws IOException {
110 			this.iWriter.flush();
111 		}
112 
113 		/**
114 		 * Writes a XML value (either attribute or text node). The value is
115 		 * escaped as follows:<br />
116 		 * <br />
117 		 * <table border="1">
118 		 * <tr>
119 		 * <th>char</th>
120 		 * <th>result</th>
121 		 * </tr>
122 		 * <tr>
123 		 * <td class="center"">&lt; space</td>
124 		 * <td>&amp;#xnn;</td>
125 		 * </tr>
126 		 * <tr>
127 		 * <td class="center"">&gt; ~</td>
128 		 * <td>unchanged (UTF-8)</td>
129 		 * </tr>
130 		 * <tr>
131 		 * <td class="center"">space</td>
132 		 * <td>unchanged or &amp;#x20;<br />
133 		 * (First leading, last trailing and every other space are escaped)</td>
134 		 * </tr>
135 		 * <tr>
136 		 * <td class="center"">&lt;</td>
137 		 * <td>&amp;lt;</td>
138 		 * </tr>
139 		 * <tr>
140 		 * <td class="center"">&gt;</td>
141 		 * <td>&amp;gt;</td>
142 		 * </tr>
143 		 * <tr>
144 		 * <td class="center"">&amp;</td>
145 		 * <td>&amp;amp;</td>
146 		 * </tr>
147 		 * <tr>
148 		 * <td class="center"">"</td>
149 		 * <td>&amp;quot;</td>
150 		 * </tr>
151 		 * <tr>
152 		 * <td class="center"">'</td>
153 		 * <td>&amp;apos;</td>
154 		 * <tr>
155 		 * <td class="center"">other</td>
156 		 * <td>unchanged</td>
157 		 * </tr>
158 		 * </tr>
159 		 * </table>
160 		 *
161 		 * @param pText
162 		 *            The text
163 		 * @throws IOException
164 		 */
165 		public void writeValue(final String pText) throws IOException {
166 			if (pText == null) {
167 				return;
168 			}
169 			boolean escapeSpace = true;
170 			final int oneBeforeLast = pText.length() - 2;
171 			for (int i = 0; i < pText.length(); ++i) {
172 				char currentChar = pText.charAt(i);
173 				boolean isSpace = false;
174 
175 				if (isHighSurrogate(currentChar)) {
176 					if (i > oneBeforeLast || !isLowSurrogate(pText.charAt(i + 1))) {
177 						throw new IOException("Illegal Unicode character");
178 					}
179 					this.iWriter.write(pText, i++, 2);
180 				} else if (currentChar < ' ') {
181 					writeAsHex(currentChar);
182 				} else if (currentChar > '~') {
183 					this.iWriter.write(currentChar);
184 				} else {
185 					switch (currentChar) {
186 						case ' ':
187 							isSpace = true;
188 							if (escapeSpace) {
189 								writeAsHex(currentChar);
190 							} else {
191 								this.iWriter.write(currentChar);
192 							}
193 							break;
194 						case '<':
195 							this.iWriter.write("&lt;");
196 							break;
197 						case '>':
198 							this.iWriter.write("&gt;");
199 							break;
200 						case '&':
201 							this.iWriter.write("&amp;");
202 							break;
203 						case '"':
204 							this.iWriter.write("&quot;");
205 							break;
206 						case '\'':
207 							this.iWriter.write("&apos;");
208 							break;
209 						default:
210 							this.iWriter.write(currentChar);
211 					}
212 				}
213 				escapeSpace = (isSpace && !escapeSpace) || (i == oneBeforeLast);
214 			}
215 		}
216 
217 		private void writeAsHex(char pChar) throws IOException {
218 			this.iWriter.write("&#x" + Integer.toHexString(pChar) + ";");
219 		}
220 
221 		private boolean isHighSurrogate(char pChar) {
222 			return pChar >= WBEMConstants.UTF16_MIN_HIGH_SURROGATE && pChar <= WBEMConstants.UTF16_MAX_HIGH_SURROGATE;
223 		}
224 
225 		private boolean isLowSurrogate(char pChar) {
226 			return pChar >= WBEMConstants.UTF16_MIN_LOW_SURROGATE && pChar <= WBEMConstants.UTF16_MAX_LOW_SURROGATE;
227 		}
228 	}
229 
230 	private boolean iPretty;
231 
232 	private int iIndent = 0;
233 
234 	private boolean iLastClosed = false;
235 
236 	private final String CDATA_START = "<![CDATA[";
237 
238 	private final String CDATA_END = "]]>";
239 
240 	private CimXmlSerializer(boolean pPretty) {
241 		this.iPretty = pPretty;
242 	}
243 
244 	/**
245 	 * Serializes a given DOM document as (CIM-)XML to a given output stream.
246 	 * The method writes first
247 	 * <code>&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;</code>
248 	 * and then serializes the document node. If you want to suppress this
249 	 * header just call {@link #serialize(OutputStream, Node, boolean)} on the
250 	 * document node.
251 	 *
252 	 * @param pOS
253 	 *            The output stream
254 	 * @param pDoc
255 	 *            The document
256 	 * @param pPretty
257 	 *            If <code>true</code> the XML is nicely wrapped and indented,
258 	 *            otherwise it's all in one line
259 	 * @throws IOException
260 	 *             Whenever something goes wrong
261 	 */
262 	public static void serialize(OutputStream pOS, Document pDoc, boolean pPretty) throws IOException {
263 		try {
264 			XmlWriter writer = new XmlWriter(pOS, WBEMConstants.UTF8);
265 			writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
266 			new CimXmlSerializer(pPretty).serializeNode(writer, pDoc.getDocumentElement());
267 			writer.flush();
268 		} catch (IOException ioe) {
269 			throw ioe;
270 		} catch (Exception e) {
271 			throw new IOException(e.getMessage());
272 		}
273 	}
274 
275 	/**
276 	 * Serializes a given DOM node as (CIM-)XML to a given output stream
277 	 *
278 	 * @param pOS
279 	 *            The output stream
280 	 * @param pNode
281 	 *            The node
282 	 * @param pPretty
283 	 *            If <code>true</code> the XML is nicely wrapped and indented,
284 	 *            otherwise it's all in one line
285 	 * @throws IOException
286 	 *             Whenever something goes wrong
287 	 */
288 	public static void serialize(OutputStream pOS, Node pNode, boolean pPretty) throws IOException {
289 		try {
290 			XmlWriter writer = new XmlWriter(pOS, WBEMConstants.UTF8);
291 			new CimXmlSerializer(pPretty).serializeNode(writer, pNode);
292 			writer.flush();
293 		} catch (IOException ioe) {
294 			throw ioe;
295 		} catch (Exception e) {
296 			throw new IOException(e.getMessage());
297 		}
298 	}
299 
300 	private void serializeNode(XmlWriter pWriter, Node pNode) throws IOException {
301 		switch (pNode.getNodeType()) {
302 			case Node.ELEMENT_NODE:
303 				pWriter.write(indent());
304 				pWriter.write("<");
305 				pWriter.write(pNode.getNodeName());
306 				NamedNodeMap attributes = pNode.getAttributes();
307 				if (attributes != null) {
308 					for (int i = 0; i < attributes.getLength(); ++i) {
309 						pWriter.write(" ");
310 						serializeNode(pWriter, attributes.item(i));
311 					}
312 				}
313 				Node child = pNode.getFirstChild();
314 				if (child == null) {
315 					pWriter.write("/>");
316 					this.iLastClosed = true;
317 					break;
318 				}
319 				pWriter.write(">");
320 				++this.iIndent;
321 				this.iLastClosed = false;
322 				while (child != null) {
323 					serializeNode(pWriter, child);
324 					child = child.getNextSibling();
325 				}
326 				--this.iIndent;
327 				if (this.iLastClosed) {
328 					pWriter.write(indent());
329 				}
330 				pWriter.write("</");
331 				pWriter.write(pNode.getNodeName());
332 				pWriter.write(">");
333 				this.iLastClosed = true;
334 				break;
335 			case Node.ATTRIBUTE_NODE:
336 				pWriter.write(pNode.getNodeName());
337 				pWriter.write("=\"");
338 				pWriter.writeValue(pNode.getNodeValue());
339 				pWriter.write("\"");
340 				break;
341 			case Node.TEXT_NODE:
342 				String value = pNode.getNodeValue();
343 				if (value != null) {
344 					int idx = 0;
345 					int len = value.length();
346 
347 					while (idx < len) {
348 						int cdata = value.indexOf(this.CDATA_START, idx);
349 
350 						// rest of string not CDATA, write all (escaped)
351 						if (cdata == -1) {
352 							pWriter.writeValue(value.substring(idx));
353 							break;
354 						}
355 
356 						// write characters before CDATA (escaped)
357 						if (idx < cdata) {
358 							pWriter.writeValue(value.substring(idx, cdata));
359 							idx = cdata;
360 						}
361 
362 						int end = value.indexOf(this.CDATA_END, idx);
363 
364 						// invalid CDATA
365 						if (end == -1) {
366 							throw new IOException("CDATA section not closed: " + value);
367 						}
368 
369 						// write CDATA (not escaped)
370 						pWriter.write(value.substring(idx, end + this.CDATA_END.length()));
371 						idx = end + this.CDATA_END.length();
372 					}
373 				}
374 		}
375 	}
376 
377 	private String indent() {
378 		if (!this.iPretty) {
379 			return "";
380 		}
381 		StringBuffer result = new StringBuffer();
382 		result.append('\n');
383 		for (int i = 0; i < this.iIndent; ++i) {
384 			result.append(' ');
385 		}
386 		return result.toString();
387 	}
388 }