/**
* Copyright 2011-2012 Alexandre Dutra
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.dutra.confluence2wordpress.xmlrpc;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import org.apache.commons.codec.EncoderException;
import org.apache.commons.codec.binary.Base64;
import org.apache.xmlrpc.DefaultTypeDecoder;
import org.apache.xmlrpc.TypeDecoder;
import org.apache.xmlrpc.XmlRpcException;
import org.apache.xmlrpc.util.DateTool;
/**
* This is a copy of the original XmlWriter from Apache XmlRpc.
* The main purpose is to get rid of the Base64 multi-threading issue.
* @see "https://issues.apache.org/jira/browse/CODEC-96"
*/
public class XmlWriter extends OutputStreamWriter
{
// Various XML pieces.
protected static final String PROLOG_START = "<?xml version=\"1.0";
protected static final String PROLOG_END = "\"?>";
protected static final String CLOSING_TAG_START = "</";
protected static final String SINGLE_TAG_END = "/>";
protected static final String LESS_THAN_ENTITY = "<";
protected static final String GREATER_THAN_ENTITY = ">";
protected static final String AMPERSAND_ENTITY = "&";
private static final char[] PROLOG =
new char[PROLOG_START.length() + PROLOG_END.length()];
static
{
int len = PROLOG_START.length();
PROLOG_START.getChars(0, len, PROLOG, 0);
PROLOG_END.getChars(0, PROLOG_END.length(), PROLOG, len);
}
/**
* Java's name for the ISO-8859-1 encoding.
*/
static final String ISO8859_1 = "ISO8859_1";
/**
* Java's name for the UTF-8 encoding.
*/
static final String UTF8 = "UTF8";
/**
* Java's name for the UTF-16 encoding.
*/
static final String UTF16 = "UTF-16";
//Base64 instances are NOT thread safe and should not be static
//https://issues.apache.org/jira/browse/CODEC-96
protected /*static*/ final Base64 base64Codec = new Base64();
/**
* Class to delegate type decoding to.
*/
protected static TypeDecoder typeDecoder;
/**
* Mapping between Java encoding names and "real" names used in
* XML prolog.
*
* @see <a href="http://java.sun.com/j2se/1.4.2/docs/guide/intl/encoding.doc.html">Java character set names</a>
*/
private static Properties encodings = new Properties();
static
{
encodings.put(UTF8, "UTF-8");
encodings.put(ISO8859_1, "ISO-8859-1");
typeDecoder = new DefaultTypeDecoder();
}
/**
* Thread-safe wrapper for the <code>DateFormat</code> object used
* to parse date/time values.
*/
DateTool dateTool = new DateTool();
/**
* Whether the XML prolog has been written.
*/
boolean hasWrittenProlog = false;
/**
* Creates a new instance.
*
* @param out The stream to write output to.
* @param enc The encoding to using for outputting XML. Only UTF-8
* and UTF-16 are supported. If another encoding is specified,
* UTF-8 will be used instead for widest XML parser
* interoperability.
* @exception UnsupportedEncodingException Since unsupported
* encodings are internally converted to UTF-8, this should only
* be seen as the result of an internal error.
*/
public XmlWriter(OutputStream out, String enc)
throws UnsupportedEncodingException
{
// Super-class wants the Java form of the encoding.
super(out, forceUnicode(enc));
}
/**
* @param encoding A caller-specified encoding.
* @return An Unicode encoding.
*/
private static String forceUnicode(String encoding)
{
if (encoding == null || !encoding.toUpperCase().startsWith("UTF"))
{
encoding = UTF8;
}
return encoding;
}
/**
* Tranforms a Java encoding to the canonical XML form (if a
* mapping is available).
*
* @param javaEncoding The name of the encoding as known by Java.
* @return The XML encoding (if a mapping is available);
* otherwise, the encoding as provided.
*
* @deprecated This method will not be visible in 2.0.
*/
protected static String canonicalizeEncoding(String javaEncoding)
{
return encodings.getProperty(javaEncoding, javaEncoding);
}
/**
* A mostly pass-through implementation wrapping
* <code>OutputStreamWriter.write()</code> which assures that the
* XML prolog is written before any other data.
*
* @see java.io.OutputStreamWriter.write(char[], int, int)
*/
public void write(char[] cbuf, int off, int len)
throws IOException
{
if (!hasWrittenProlog)
{
super.write(PROLOG, 0, PROLOG.length);
hasWrittenProlog = true;
}
super.write(cbuf, off, len);
}
/**
* A mostly pass-through implementation wrapping
* <code>OutputStreamWriter.write()</code> which assures that the
* XML prolog is written before any other data.
*
* @see java.io.OutputStreamWriter.write(char)
*/
public void write(char c)
throws IOException
{
if (!hasWrittenProlog)
{
super.write(PROLOG, 0, PROLOG.length);
hasWrittenProlog = true;
}
super.write(c);
}
/**
* A mostly pass-through implementation wrapping
* <code>OutputStreamWriter.write()</code> which assures that the
* XML prolog is written before any other data.
*
* @see java.io.OutputStreamWriter.write(String, int, int)
*/
public void write(String str, int off, int len)
throws IOException
{
if (!hasWrittenProlog)
{
super.write(PROLOG, 0, PROLOG.length);
hasWrittenProlog = true;
}
super.write(str, off, len);
}
/**
* Writes the XML representation of a supported Java object type.
*
* @param obj The <code>Object</code> to write.
* @exception XmlRpcException Unsupported character data found.
* @exception IOException Problem writing data.
* @throws IllegalArgumentException If a <code>null</code>
* parameter is passed to this method (not supported by the <a
* href="http://xml-rpc.com/spec">XML-RPC specification</a>).
*/
public void writeObject(Object obj)
throws XmlRpcException, IOException
{
startElement("value");
if (obj == null)
{
throw new XmlRpcException
(0, "null values not supported by XML-RPC");
}
else if (obj instanceof String)
{
chardata(obj.toString());
}
else if (typeDecoder.isXmlRpcI4(obj))
{
startElement("int");
write(obj.toString());
endElement("int");
}
else if (obj instanceof Boolean)
{
startElement("boolean");
write(((Boolean) obj).booleanValue() ? "1" : "0");
endElement("boolean");
}
else if (typeDecoder.isXmlRpcDouble(obj))
{
startElement("double");
write(obj.toString());
endElement("double");
}
else if (obj instanceof Date)
{
startElement("dateTime.iso8601");
Date d = (Date) obj;
write(dateTool.format(d));
endElement("dateTime.iso8601");
}
else if (obj instanceof byte[])
{
startElement("base64");
try
{
this.write((byte[]) base64Codec.encode(obj));
}
catch (EncoderException e)
{
throw new XmlRpcException
(0, "Unable to Base 64 encode byte array", e);
}
endElement("base64");
}
else if (obj instanceof Object[])
{
startElement("array");
startElement("data");
Object[] array = (Object []) obj;
for (int i = 0; i < array.length; i++)
{
writeObject(array[i]);
}
endElement("data");
endElement("array");
}
else if (obj instanceof List<?>)
{
startElement("array");
startElement("data");
List<?> list = (List<?>) obj;
int size = list.size();
for (int i = 0; i < size; i++)
{
writeObject(list.get(i));
}
endElement("data");
endElement("array");
}
else if (obj instanceof Map<?,?>)
{
startElement("struct");
Map<?,?> struct = (Map<?,?>) obj;
for (Entry<?,?> entry : struct.entrySet()) {
String key = (String) entry.getKey();
Object value = entry.getValue();
startElement("member");
startElement("name");
chardata(key);
endElement("name");
writeObject(value);
endElement("member");
}
endElement("struct");
}
else
{
throw new XmlRpcException(0, "Unsupported Java type: "
+ obj.getClass(), null);
}
endElement("value");
}
/**
* This is used to write out the Base64 output...
*/
protected void write(byte[] byteData) throws IOException
{
for (int i = 0; i < byteData.length; i++)
{
write(byteData[i]);
}
}
/**
* Writes characters like '\r' (0xd) as " ".
*/
private void writeCharacterReference(char c)
throws IOException
{
write("");
write(String.valueOf((int) c));
write(';');
}
/**
*
* @param elem
* @throws IOException
*/
protected void startElement(String elem) throws IOException
{
write('<');
write(elem);
write('>');
}
/**
*
* @param elem
* @throws IOException
*/
protected void endElement(String elem) throws IOException
{
write(CLOSING_TAG_START);
write(elem);
write('>');
}
/**
*
* @param elem
* @throws IOException
*/
protected void emptyElement(String elem) throws IOException
{
write('<');
write(elem);
write(SINGLE_TAG_END);
}
/**
* Writes text as <code>PCDATA</code>.
*
* @param text The data to write.
* @exception XmlRpcException Unsupported character data found.
* @exception IOException Problem writing data.
*/
protected void chardata(String text)
throws XmlRpcException, IOException
{
int l = text.length ();
// ### TODO: Use a buffer rather than going character by
// ### character to scale better for large text sizes.
//char[] buf = new char[32];
for (int i = 0; i < l; i++)
{
char c = text.charAt (i);
switch (c)
{
case '\t':
case '\n':
write(c);
break;
case '\r':
// Avoid normalization of CR to LF.
writeCharacterReference(c);
break;
case '<':
write(LESS_THAN_ENTITY);
break;
case '>':
write(GREATER_THAN_ENTITY);
break;
case '&':
write(AMPERSAND_ENTITY);
break;
default:
// Though the XML spec requires XML parsers to support
// Unicode, not all such code points are valid in XML
// documents. Additionally, previous to 2003-06-30
// the XML-RPC spec only allowed ASCII data (in
// <string> elements). For interoperability with
// clients rigidly conforming to the pre-2003 version
// of the XML-RPC spec, we entity encode characters
// outside of the valid range for ASCII, too.
if (c > 0x7f || !isValidXMLChar(c))
{
// Replace the code point with a character reference.
writeCharacterReference(c);
}
else
{
write(c);
}
}
}
}
/**
* Section 2.2 of the XML spec describes which Unicode code points
* are valid in XML:
*
* <blockquote><code>#x9 | #xA | #xD | [#x20-#xD7FF] |
* [#xE000-#xFFFD] | [#x10000-#x10FFFF]</code></blockquote>
*
* Code points outside this set must be entity encoded to be
* represented in XML.
*
* @param c The character to inspect.
* @return Whether the specified character is valid in XML.
*/
private static final boolean isValidXMLChar(char c)
{
switch (c)
{
case 0x9:
case 0xa: // line feed, '\n'
case 0xd: // carriage return, '\r'
return true;
default:
return ( (0x20 <= c && c <= 0xd7ff) ||
(0xe000 <= c && c <= 0xfffd) ||
(0x10000 <= c && c <= 0x10ffff) );
}
}
protected static void setTypeDecoder(TypeDecoder newTypeDecoder)
{
typeDecoder = newTypeDecoder;
}
}