/*******************************************************************************
* Copyright (c) 2006-2010 eBay Inc. All Rights Reserved.
* 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
*******************************************************************************/
package org.ebayopensource.turmeric.runtime.binding.impl.parser.nv;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamException;
import org.ebayopensource.turmeric.runtime.binding.BindingConstants;
import org.ebayopensource.turmeric.runtime.binding.impl.parser.BaseStreamWriter;
import org.ebayopensource.turmeric.runtime.binding.impl.parser.IndexedQName;
import org.ebayopensource.turmeric.runtime.binding.impl.parser.NamespaceConvention;
import org.ebayopensource.turmeric.runtime.binding.schema.DataElementSchema;
import org.ebayopensource.turmeric.runtime.binding.utils.BufferedCharWriter;
import org.ebayopensource.turmeric.runtime.binding.utils.CollectionUtils;
import com.ebay.kernel.util.FastURLEncoder;
/**
* @author wdeng
*/
public class NVStreamWriter extends BaseStreamWriter {
public static final String KEY_USE_BRACKETS = "useBrackets";
public static final String KEY_QUOTE_VALUE = "quoteValue";
public static final String KEY_ENCODE_VALUE = "encodeValue";
public static final String KEY_FULL_URL_ENCODED = "fullURLEncoded";
public static final String KEY_ALWAYS_ADD_PREFIX = "alwaysAddPrefix";
public static final String KEY_USE_SCHEMA_INFO = "useSchemaInfo";
private static final String[] INTEGER_STRINGS = new String[] { "0",
"1", "2", "3", "4", "5", "6", "7", "8", "9" };
private final ArrayList<IndexedQName> m_names;
private final BufferedCharWriter m_os;
private boolean m_shouldAddAmpersand = false;
private IndexedQName m_prevQName;
private boolean m_shouldOutputNamespaceDefs = true;
private QName m_rootXmlName;
private char[] m_indexChars = new char[] {'(',')'};
private boolean m_quoteValue = true;
private boolean m_encodeValue = true;
private boolean m_alwaysAddPrefix = false;
private boolean m_skipPrefix = false;
private boolean m_fullURLEncoding = false;
private boolean m_useSchemaInfo = false;
private String m_singleNamespacePrefix;
private Charset m_charset;
private DataElementSchema m_rootEleSchema;
private ArrayList<DataElementSchema> m_elementPath;
public NVStreamWriter(NamespaceConvention conv, QName rootXmlName, OutputStream os)
throws XMLStreamException {
this(conv, os, Charset.forName("UTF-8"), rootXmlName, null, CollectionUtils.EMPTY_STRING_MAP, false);
}
public NVStreamWriter(NamespaceConvention convention, OutputStream os,
Charset charset, QName rootXmlName, DataElementSchema rootEleSchema,
Map<String, String>options, boolean fullURLEncoding)
throws XMLStreamException
{
super(convention);
m_names = new ArrayList<IndexedQName>(32);
m_os = new BufferedCharWriter(os, charset, 2048);
m_charset = charset;
m_rootXmlName = rootXmlName;
setupOptions(options);
m_fullURLEncoding = fullURLEncoding;
m_skipPrefix = !m_alwaysAddPrefix && convention.isSingleNamespace();
if (m_skipPrefix && convention.isSingleNamespace()) {
m_singleNamespacePrefix = convention.getPrefix(convention.getSingleNamespace());
}
if (m_useSchemaInfo) {
m_elementPath = new ArrayList<DataElementSchema>();
m_rootEleSchema = rootEleSchema;
}
}
private void setupOptions(Map<String, String>options) {
if (null == options) {
return;
}
String useBracketsOption = options.get(KEY_USE_BRACKETS);
if (useBracketsOption != null && Boolean.parseBoolean(useBracketsOption)) {
m_indexChars[0] = '[';
m_indexChars[1] = ']';
}
String quoteValueOption = options.get(KEY_QUOTE_VALUE);
if (null != quoteValueOption) {
m_quoteValue = Boolean.parseBoolean(quoteValueOption);
}
String encodeValueOption = options.get(KEY_ENCODE_VALUE);
if (null != encodeValueOption) {
m_encodeValue = Boolean.parseBoolean(encodeValueOption);
}
String alwaysAddPrefixOption = options.get(KEY_ALWAYS_ADD_PREFIX);
if (null != alwaysAddPrefixOption) {
m_alwaysAddPrefix = Boolean.parseBoolean(alwaysAddPrefixOption);
}
String useSchemaInfoOption = options.get(KEY_USE_SCHEMA_INFO);
if (null != useSchemaInfoOption) {
m_useSchemaInfo = Boolean.parseBoolean(useSchemaInfoOption);
}
}
@Override
public void close() throws XMLStreamException {
try {
m_os.close();
} catch (IOException e) {
throw new XMLStreamException(e);
}
}
@Override
public void flush() throws XMLStreamException {
try {
m_os.flush();
} catch (IOException e) {
throw new XMLStreamException(e);
}
}
@Override
public void writeCharacters(String value) throws XMLStreamException {
writeNVPair(null, null, value);
}
private void writeNVPair(String nsURI, String localName, String value) throws XMLStreamException {
try {
if (m_shouldAddAmpersand) {
m_os.write('&');
} else {
m_shouldAddAmpersand = true;
}
int len = m_names.size();
// This is to handle simple argument type.
boolean isSimpleType = (len == 0);
if (isSimpleType) {
m_names.add(new IndexedQName(m_rootXmlName, 0));
len = 1;
}
if (m_useSchemaInfo) {
writeNVPathWithSchemaInfo(nsURI, localName, value, isSimpleType);
} else {
writeNVPathWithoutSchemaInfo(nsURI, localName, value);
}
if (localName != null) {
m_os.write('.');
writeOneName(nsURI, localName);
}
if (value != null) {
m_os.write("=");
if (m_quoteValue) {
m_os.write('\"');
}
if (m_fullURLEncoding) {
writeURLEncodedValue(value);
} else {
if (m_encodeValue) {
writeURLEncodedValue(value);
} else if (m_quoteValue) {
writeDoubleQuoteEncodedValue(value);
} else {
writeNVEncodedValue(value);
}
}
if (m_quoteValue) {
m_os.write('\"');
}
} else {
m_os.write("=null");
}
} catch (IOException ioe) {
throw new XMLStreamException(ioe);
}
}
private void writeNVPathWithSchemaInfo(String nsURI, String localName, String value, boolean isSimpleType)
throws XMLStreamException, IOException {
DataElementSchema currentElement = null;
Iterator<DataElementSchema> elementIter = m_elementPath.iterator();
// NV format always skips the root element if it is not simple type object.
if (!isSimpleType) {
elementIter.next();
}
for (int i = 0; i < m_names.size(); i++) {
IndexedQName name = m_names.get(i);
if (elementIter.hasNext()) {
currentElement = elementIter.next();
}
if (i > 0) {
m_os.write('.');
}
writeOneName(name.getNamespaceURI(), name.getLocalPart());
int maxOccurs = currentElement == null ? BindingConstants.UNBOUNDED : currentElement.getMaxOccurs();
if (maxOccurs > 1 || maxOccurs == BindingConstants.UNBOUNDED) {
writeIndex(name.getIndex());
}
}
}
private void writeNVPathWithoutSchemaInfo(String nsURI, String localName, String value)
throws XMLStreamException, IOException {
int len = m_names.size();
for (int i = 0; i < len; i++) {
IndexedQName name = m_names.get(i);
if (i > 0) {
m_os.write('.');
}
writeOneName(name.getNamespaceURI(), name.getLocalPart());
writeIndex(name.getIndex());
}
}
private void writeIndex(int index) throws IOException {
m_os.write(m_indexChars[0]);
if (index <= 9) {
// index cannot be negative in this writer
m_os.write(INTEGER_STRINGS[index]);
} else {
m_os.write(Integer.toString(index));
}
m_os.write(m_indexChars[1]);
}
private String writeOneName(String nsURI, String localName) throws IOException {
String prefix = m_convention.getPrefix(nsURI);
if ((!m_skipPrefix || !prefix.equals(m_singleNamespacePrefix)) && !"".equals(prefix)) {
m_os.write(prefix);
m_os.write(':');
}
m_os.write(localName);
return prefix;
}
@Override
public void writeAttribute(String prefix, String nsURI, String localName,
String value) throws XMLStreamException {
writeNVPair(nsURI, BindingConstants.ATTRIBUTE_MARK + localName, value);
}
@Override
public void writeStartElement(String prefix, String localName,
String namespaceURI) throws XMLStreamException
{
if (null == localName) {
throw new XMLStreamException(
"writeStartElement expects non-null local name.");
}
int index = 0;
if (null != m_prevQName
&& m_prevQName.sameQName(namespaceURI, localName, prefix)) {
index = m_prevQName.getIndex() + 1;
}
IndexedQName elementName = new IndexedQName(namespaceURI, localName, prefix, index);
// Find the DataElementSchema and add it to the stack.
if (m_useSchemaInfo) {
setupSchemaInfo(elementName);
}
if (elementName.getQName().equals(m_rootXmlName)) {
return;
}
m_names.add(elementName);
}
private void setupSchemaInfo(IndexedQName elementName) throws XMLStreamException {
DataElementSchema eleSchema = null;
QName qName = elementName.getQName();
if (m_elementPath.isEmpty()) {
eleSchema = m_rootEleSchema;
} else {
DataElementSchema parentEleSchema = m_elementPath.get(m_elementPath.size() - 1);
if(!parentEleSchema.hasChildren()) {
throw new XMLStreamException(
String.format("Unable to load schema \"%s\" - parent schema \"%s\" has no children",
qName, parentEleSchema.getElementName()));
}
eleSchema = parentEleSchema.getChild(qName);
if (eleSchema == null) {
throw new XMLStreamException(
String.format("Unable to load schema \"%s\" - Not a child of parent schema \"%s\"",
qName, parentEleSchema.getElementName()));
}
}
if (eleSchema == null) {
throw new XMLStreamException("Unable to load schema information for: " + qName);
}
m_elementPath.add(eleSchema);
}
@Override
public void writeEndElement() throws XMLStreamException {
if (m_names.size() < 1) {
return;
}
if (m_useSchemaInfo) {
m_elementPath.remove(m_elementPath.size() - 1);
}
m_prevQName = m_names.remove(m_names.size() - 1);
}
public void writeStartDocument() throws XMLStreamException {
if (!m_shouldOutputNamespaceDefs) {
return;
}
m_shouldOutputNamespaceDefs = false;
// If it is single namespace message, don't output NS definition.
if (m_skipPrefix) {
return;
}
Map<String, String> prefixToNSMap = m_convention
.getPrefixToNamespaceMap();
for (Map.Entry<String,String> e: prefixToNSMap.entrySet()) {
String prefix = e.getKey();
String ns = e.getValue();
outputNamespace(prefix, ns);
}
}
@Override
public void writeDefaultNamespace(String arg0) throws XMLStreamException {
m_convention.setSingleNamespace(arg0);
}
private void outputNamespace(String prefix, String ns)
throws XMLStreamException
{
try {
if (m_shouldAddAmpersand) {
m_os.write('&');
} else {
m_shouldAddAmpersand = true;
}
m_os.write(NVConstants.NV_NAMESPACE_DEF_PREFIX);
if (!(null == prefix) && !"".equals(prefix)) {
m_os.write(':');
m_os.write(prefix);
}
m_os.write("=");
if( m_quoteValue ) {
m_os.write("\"");
}
m_os.write(ns);
if( m_quoteValue ) {
m_os.write('\"');
}
} catch (IOException ioe) {
throw new XMLStreamException(ioe);
}
}
private static final String[] CHAR_ENCODE = new String[256];
private static final String[] URL_CHAR_ENCODE = new String[256];
static {
for (int i=0; i<CHAR_ENCODE.length; i++) {
CHAR_ENCODE[i] = "" + (char)i;
}
CHAR_ENCODE[' '] = "+"; // Only need to encode the
CHAR_ENCODE['&'] = "%26"; // special character that
CHAR_ENCODE['='] = "%3d"; // NV format is using.
CHAR_ENCODE['.'] = "%2e";
CHAR_ENCODE[':'] = "%3a";
CHAR_ENCODE['"'] = "%22";
for (int i = -128; i < 128; i++) {
char c = (char)i;
if ( (c >= 'a' && c <= 'z')
|| (c >= 'A' && c <= 'Z')
|| (c >= '0' && c <= '9')
|| c == '.'
|| c == '-'
|| c == '_'
|| c == '*'
) {
URL_CHAR_ENCODE[0xff & i] = "" + c;
}
else if (c == ' ') {
URL_CHAR_ENCODE[0xff & i] = "" + '+';
}
else {
URL_CHAR_ENCODE[0xff & i] = getStandardEncodedChar(c);
}
}
}
private static String getStandardEncodedChar(char c) {
String encodedStr = Integer.toHexString(c);
int size = encodedStr.length();
if (size >= 2) {
encodedStr = encodedStr.substring(size - 2, size);
return "%" + encodedStr.toUpperCase();
}
return "%0" + encodedStr.toUpperCase();
}
// This method try to minimize the URLencoding that we are doing based on
// m_charset. we URL encode any character whose character value < 256.
// any character > 256 we give them to the java writer to handle the
// encoding.
// This will not work for services that uses non-SOA client.
private void writeNVEncodedValue(String value) throws IOException {
if (null == value) {
return;
}
for (int i=0; i<value.length(); i++) {
char code = value.charAt(i);
if (code < 256) {
m_os.write(CHAR_ENCODE[code]);
} else {
m_os.write(code);
}
}
}
private void writeURLEncodedValue(String value) throws IOException {
if (null == value) {
return;
}
String encodedValue = FastURLEncoder.encode(value, m_charset.name());
m_os.write(encodedValue);
}
private void writeDoubleQuoteEncodedValue(String value) throws IOException {
if (null == value) {
return;
}
for (int i=0; i<value.length(); i++) {
char code = value.charAt(i);
if (code == '"') {
m_os.write("%22");
} else {
m_os.write(code);
}
}
}
public void setFullURLEncoding(boolean fullURLEncoded) {
this.m_fullURLEncoding = fullURLEncoded;
}
}