/******************************************************************************* * Copyright (c) 1998, 2016 Oracle and/or its affiliates. All rights reserved. * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 * which accompanies this distribution. * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html * and the Eclipse Distribution License is available at * http://www.eclipse.org/org/documents/edl-v10.php. * * Contributors: * Oracle - initial API and implementation from Oracle TopLink ******************************************************************************/ package org.eclipse.persistence.oxm.record; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.nio.charset.Charset; import java.nio.charset.CharsetEncoder; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.eclipse.persistence.exceptions.XMLMarshalException; import org.eclipse.persistence.internal.oxm.CharacterEscapeHandler; import org.eclipse.persistence.internal.oxm.Constants; import org.eclipse.persistence.internal.oxm.NamespaceResolver; import org.eclipse.persistence.internal.oxm.XMLMarshaller; import org.eclipse.persistence.internal.oxm.XPathFragment; import org.eclipse.persistence.internal.oxm.record.ExtendedContentHandler; import org.eclipse.persistence.internal.oxm.record.XMLFragmentReader; import org.w3c.dom.Attr; import org.w3c.dom.Node; import org.xml.sax.Attributes; import org.xml.sax.Locator; import org.xml.sax.SAXException; import org.xml.sax.ext.LexicalHandler; /** * <p>Use this type of MarshalRecord when the marshal target is a Writer and the * XML should not be formatted with carriage returns or indenting.</p> * <p><code> * XMLContext xmlContext = new XMLContext("session-name");<br> * XMLMarshaller xmlMarshaller = xmlContext.createMarshaller();<br> * WriterRecord writerRecord = new WriterRecord();<br> * writerRecord.setWriter(myWriter);<br> * xmlMarshaller.marshal(myObject, writerRecord);<br> * </code></p> * <p>If the marshal(Writer) and setFormattedOutput(false) method is called on * XMLMarshaller, then the Writer is automatically wrapped in a * WriterRecord.</p> * <p><code> * XMLContext xmlContext = new XMLContext("session-name");<br> * XMLMarshaller xmlMarshaller = xmlContext.createMarshaller();<br> * xmlMarshaller xmlMarshaller.setFormattedOutput(false);<br> * xmlMarshaller.marshal(myObject, myWriter);<br> * </code></p> * @see org.eclipse.persistence.oxm.XMLMarshaller */ public class WriterRecord extends MarshalRecord<XMLMarshaller> { protected StringBuilder builder = new StringBuilder(3072); protected boolean isStartElementOpen = false; protected boolean isProcessingCData = false; protected CharsetEncoder encoder; String charset = "UNKNOWN_CHARSET"; private static final String defaultCharset = Constants.DEFAULT_XML_ENCODING.intern(); private Writer writer; /** * Return the Writer that the object will be marshalled to. * @return The marshal target. */ public Writer getWriter() { return writer; } /** * Set the Writer that the object will be marshalled to. * @param writer The marshal target. */ public void setWriter(Writer writer) { this.writer = writer; } /** * INTERNAL: */ public void startDocument(String encoding, String version) { builder.append("<?xml version=\""); builder.append(version); builder.append('\"'); if (null != encoding) { builder.append(" encoding=\""); builder.append(encoding); builder.append('\"'); } builder.append("?>"); } /** * INTERNAL */ public void writeHeader() { builder.append(getMarshaller().getXmlHeader()); } /** * INTERNAL: */ public void endDocument() {} /** * INTERNAL: */ public void openStartElement(XPathFragment xPathFragment, NamespaceResolver namespaceResolver) { super.openStartElement(xPathFragment, namespaceResolver); if (isStartElementOpen) { builder.append('>'); } isStartElementOpen = true; builder.append('<'); builder.append(getNameForFragment(xPathFragment)); if(xPathFragment.isGeneratedPrefix()){ namespaceDeclaration(xPathFragment.getPrefix(), xPathFragment.getNamespaceURI()); } } /** * INTERNAL: */ public void element(XPathFragment frag) { if (isStartElementOpen) { builder.append('>'); isStartElementOpen = false; } builder.append('<'); builder.append(getNameForFragment(frag)); builder.append('/'); builder.append('>'); } /** * INTERNAL: */ public void attribute(XPathFragment xPathFragment, NamespaceResolver namespaceResolver, String value) { attribute(null, xPathFragment.getLocalName(), getNameForFragment(xPathFragment), value); } /** * INTERNAL: * override so we don't iterate over namespaces when startPrefixMapping doesn't do anything */ public void startPrefixMappings(NamespaceResolver namespaceResolver) { } /** * INTERNAL: */ public void attribute(String namespaceURI, String localName, String qName, String value) { builder.append(' '); builder.append(qName); builder.append('='); builder.append('\"'); writeValue(value, true, this.builder); builder.append('\"'); } /** * INTERNAL: */ public void closeStartElement() {} /** * INTERNAL: */ public void endElement(XPathFragment xPathFragment, NamespaceResolver namespaceResolver) { if (isStartElementOpen) { builder.append('/'); builder.append('>'); isStartElementOpen = false; } else { builder.append('<'); builder.append('/'); builder.append(getNameForFragment(xPathFragment)); builder.append('>'); } isStartElementOpen = false; } /** * INTERNAL: */ public void characters(String value) { if (isStartElementOpen) { isStartElementOpen = false; builder.append('>'); } writeValue(value); } /** * INTERNAL: */ public void cdata(String value) { if(isStartElementOpen) { isStartElementOpen = false; builder.append('>'); } for (String part : MarshalRecord.splitCData(value)) { builder.append("<![CDATA["); builder.append(part); builder.append("]]>"); } } /** * INTERNAL: */ protected void writeValue(String value) { writeValue(value, false, this.builder); } /** * INTERNAL: */ protected void writeValue(String value, boolean isAttribute, StringBuilder writer) { CharacterEscapeHandler escapeHandler = null; if (marshaller != null) { escapeHandler = marshaller.getCharacterEscapeHandler(); } if (escapeHandler != null) { try { StringWriter sw = new StringWriter(); escapeHandler.escape(value.toCharArray(), 0, value.length(), isAttribute, sw); writer.append(sw.toString()); } catch (IOException e) { throw XMLMarshalException.marshalException(e); } return; } if(null == encoder) { encoder = Constants.DEFAULT_CHARSET.newEncoder(); charset = defaultCharset; } char[] chars = value.toCharArray(); int nClosingSquareBracketsInRow = 0; for (int x = 0, charsSize = chars.length; x < charsSize; x++) { char character = chars[x]; switch (character) { case '&': { writer.append("&"); break; } case '<': { writer.append("<"); break; } case '>': { if (nClosingSquareBracketsInRow >= 2) { writer.append(">"); } else { writer.append(character); } break; } case '"': { writer.append("""); break; } case '\n' : { if(isAttribute) { writer.append(" "); } else { writer.append('\n'); } break; } case '\r': { writer.append(" "); break; } default: if (charset == defaultCharset) { writer.append(character); } else { if(encoder.canEncode(character)) { writer.append(character); } else { writer.append("&#"); writer.append(Integer.toString(character)); writer.append(';'); } } } if (!isAttribute) { // count ] to escape ]]> if (']' == character) { ++nClosingSquareBracketsInRow; } else { nClosingSquareBracketsInRow = 0; } } } } /** * Receive notification of a node. * @param node The Node to be added to the document * @param namespaceResolver The NamespaceResolver can be used to resolve the * namespace URI/prefix of the node */ public void node(Node node, NamespaceResolver namespaceResolver, String newNamespace, String newName) { if (node.getNodeType() == Node.ATTRIBUTE_NODE) { Attr attr = (Attr) node; String resolverPfx = null; if (getNamespaceResolver() != null) { resolverPfx = this.getNamespaceResolver().resolveNamespaceURI(attr.getNamespaceURI()); } // If the namespace resolver contains a prefix for the attribute's URI, // use it instead of what is set on the attribute if (resolverPfx != null) { attribute(attr.getNamespaceURI(), Constants.EMPTY_STRING, resolverPfx+Constants.COLON+attr.getLocalName(), attr.getNodeValue()); } else { attribute(attr.getNamespaceURI(), Constants.EMPTY_STRING, attr.getName(), attr.getNodeValue()); // May need to declare the URI locally if (attr.getNamespaceURI() != null) { namespaceDeclaration(attr.getPrefix(), attr.getNamespaceURI()); this.getNamespaceResolver().put(attr.getPrefix(), attr.getNamespaceURI()); } } } else if (node.getNodeType() == Node.TEXT_NODE) { characters(node.getNodeValue()); } else { try { WriterRecordContentHandler wrcHandler = new WriterRecordContentHandler(); XMLFragmentReader xfragReader = new XMLFragmentReader(namespaceResolver); xfragReader.setContentHandler(wrcHandler); xfragReader.setProperty("http://xml.org/sax/properties/lexical-handler", wrcHandler); xfragReader.parse(node, newNamespace, newName); } catch (SAXException sex) { throw XMLMarshalException.marshalException(sex); } } } /** * This class will typically be used in conjunction with an XMLFragmentReader. * The XMLFragmentReader will walk a given XMLFragment node and report events * to this class - the event's data is then written to the enclosing class' * writer. * * @see org.eclipse.persistence.internal.oxm.record.XMLFragmentReader */ protected class WriterRecordContentHandler implements ExtendedContentHandler, LexicalHandler { Map<String, String> prefixMappings; WriterRecordContentHandler() { prefixMappings = new HashMap<String, String>(); } // --------------------- CONTENTHANDLER METHODS --------------------- // public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException { if (isStartElementOpen) { builder.append('>'); } builder.append('<'); builder.append(qName); isStartElementOpen = true; // Handle attributes handleAttributes(atts); // Handle prefix mappings writePrefixMappings(); } public void endElement(String namespaceURI, String localName, String qName) throws SAXException { if (isStartElementOpen) { builder.append('/'); builder.append('>'); } else { builder.append('<'); builder.append('/'); builder.append(qName); builder.append('>'); } isStartElementOpen = false; } public void startPrefixMapping(String prefix, String uri) throws SAXException { String namespaceUri = getNamespaceResolver().resolveNamespacePrefix(prefix); if(namespaceUri == null || !namespaceUri.equals(uri)) { prefixMappings.put(prefix, uri); } } public void characters(char[] ch, int start, int length) throws SAXException { String characters = new String (ch, start, length); characters(characters); } public void characters(CharSequence characters) throws SAXException { if (isProcessingCData) { cdata(characters.toString()); return; } if (isStartElementOpen) { builder.append('>'); isStartElementOpen = false; } writeValue(characters.toString()); } // --------------------- LEXICALHANDLER METHODS --------------------- // public void comment(char[] ch, int start, int length) throws SAXException { if (isStartElementOpen) { builder.append('>'); isStartElementOpen = false; } writeComment(ch, start, length); } public void startCDATA() throws SAXException { isProcessingCData = true; } public void endCDATA() throws SAXException { isProcessingCData = false; } // --------------------- CONVENIENCE METHODS --------------------- // protected void writePrefixMappings() { if (!prefixMappings.isEmpty()) { Set<Entry<String, String>> entries = prefixMappings.entrySet(); Iterator<Entry<String, String>> iter = entries.iterator(); while(iter.hasNext()){ Entry<String, String> nextEntry = iter.next(); String prefix = nextEntry.getKey(); builder.append(' '); builder.append(javax.xml.XMLConstants.XMLNS_ATTRIBUTE); if(null != prefix && prefix.length() > 0) { builder.append(Constants.COLON); builder.append(prefix); } builder.append('='); builder.append('"'); String uri = nextEntry.getValue(); if(null != uri) { builder.append(uri); } builder.append('"'); } prefixMappings.clear(); } } protected void handleAttributes(Attributes atts) { for (int i=0, attsLength = atts.getLength(); i<attsLength; i++) { String qName = atts.getQName(i); if((qName != null && (qName.startsWith(javax.xml.XMLConstants.XMLNS_ATTRIBUTE + Constants.COLON) || qName.equals(javax.xml.XMLConstants.XMLNS_ATTRIBUTE)))) { continue; } attribute(atts.getURI(i), atts.getLocalName(i), qName, atts.getValue(i)); } } protected void writeComment(char[] chars, int start, int length) { builder.append('<'); builder.append('!'); builder.append('-'); builder.append('-'); for (int x = start; x < length; x++) { builder.append(chars[x]); } builder.append('-'); builder.append('-'); builder.append('>'); } protected void writeCharacters(char[] chars, int start, int length) { for (int x = start; x < length; x++) { builder.append(chars[x]); } } // --------------- SATISFY CONTENTHANDLER INTERFACE --------------- // public void endPrefixMapping(String prefix) throws SAXException {} public void processingInstruction(String target, String data) throws SAXException {} public void setDocumentLocator(Locator locator) {} public void startDocument() throws SAXException {} public void endDocument() throws SAXException {} public void skippedEntity(String name) throws SAXException {} public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {} // --------------- SATISFY LEXICALHANDLER INTERFACE --------------- // public void startEntity(String name) throws SAXException {} public void endEntity(String name) throws SAXException {} public void startDTD(String name, String publicId, String systemId) throws SAXException {} public void endDTD() throws SAXException {} @Override public void setNil(boolean isNil) {} } @Override public void setMarshaller(XMLMarshaller marshaller) { super.setMarshaller(marshaller); if (charset == marshaller.getEncoding()) { encoder.reset(); } else { encoder = Charset.forName(marshaller.getEncoding()).newEncoder(); charset = marshaller.getEncoding(); } } @Override public void flush() { try { writer.write(builder.toString()); builder.setLength(0); writer.flush(); } catch (IOException e) { throw XMLMarshalException.marshalException(e); } } }