// // Copyright (c)1998-2011 Pearson Education, Inc. or its affiliate(s). // All rights reserved. // package openadk.library; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.io.*; import openadk.library.common.SIF_ExtendedElement; import openadk.library.common.XMLData; import openadk.library.impl.SIFIOFormatter; import openadk.library.impl.surrogates.RenderSurrogate; import openadk.util.XMLWriter; /** * Renders a <code>SIFElement</code> to an XML stream in SIF format.<p> * * Agents do not typically use the SIFWriter class directly, but may do so to * render a SIF Data Object or a SIF Message to an output stream. The following * code demonstrates how to write a SIFDataObject to System.out: * <p> * * <pre> * StudentPersonal sp = ... * SIFWriter out = new SIFWriter( System.out ); * out.write( sp ); * </pre> * @author Eric Petersen * @version 1.0 */ public class SIFWriter { private boolean fRootAttributesWritten = false; private XMLWriter fWriter; /** * The SIFElement to render */ protected SIFElement fData; /** * Elements that should not be included in the output */ protected HashMap<String, ElementDef> fFilter; /** * The version of SIF to use when rendering XML */ protected SIFVersion fVersion; protected SIFFormatter fFormatter; /** * The sif3 namespace tag */ protected String nsTag; public static final String XSI_NAMESPACE = "http://www.w3.org/2001/XMLSchema-instance"; public static final String XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace"; public static final String NIL = "nil"; public static final String XSI_NIL = "xsi:" + NIL; private SIFWriter() { setSIFVersion( ADK.getSIFVersion() ); } /** * Creates an instance of SIFWriter to wrap the given stream * @param out The OutputStream to write to, using SIF Encoding rules */ public SIFWriter( OutputStream out ) { this(); fWriter = new XMLWriter( SIFIOFormatter.createOutputWriter( out ) ); } /** * Creates an instance of SIFWriter to wrap the given stream * @param out The OutputStream to write elements to, using SIF Encoding rules * @param autoFlush true to flush the stream after each write */ public SIFWriter( OutputStream out, boolean autoFlush ) { this(); fWriter = new XMLWriter( SIFIOFormatter.createOutputWriter( out ), autoFlush ); } /** * Creates an instance of SIFWriter to wrap the given stream. This * constructor allows agent properties to be passed to the SIFWriter * <p> * The agent property, <code>adk.compatibility.enableXMLEscaping</code> is * checked in this constructor. If the falue is false, XmlElement and * Attribute data will not be escaped * @param out The OutputStream to write elements to * @param zone the Zone to read properties from */ public SIFWriter( OutputStream out, Zone zone ) { this(); fWriter = new XMLWriter( SIFIOFormatter.createOutputWriter( out ) ); } /** * Constructor * @param out The Writer to write elements to */ public SIFWriter( Writer out ) { this(); fWriter = new XMLWriter( out ); } /** * Constructor that allows agent properties to be passed to the SIFWriter * <p> * The agent property, <code>adk.compatibility.enableXMLEscaping</code> is checked in this constructor. * If the falue is false, XmlElement and Attribute data will not be escaped * @param out the Writer to write elements to * @param zone the Zone to read properties from */ public SIFWriter( Writer out, Zone zone ) { this(); fWriter = new XMLWriter( out ); } /** * Constructor * @param out The Writer to write elements to * @param autoFlush true to flush the Writer after each write */ public SIFWriter( Writer out, boolean autoFlush ) { this(); fWriter = new XMLWriter( out, autoFlush ); } /** * Places a filter on this SIFWriter such that only elements (and their * children) identified in the array will be included in the output. Note * that attributes are always included even if not specified in the filter * list, as are top-level SIF Data Objects like StudentPersonal.<p> * * The filter remains in effect until the <code>clearFilter</code> method * is called or a null array is passed to this method.<p> * * @param elements An array of ElementDef constants from the SIFDTD class * that identify elements to include in the output, or <code>null</code> * to clear the current filter */ public void setFilter( ElementDef[] elements ) { if( elements == null ) clearFilter(); else { if( fFilter == null ) fFilter = new HashMap<String,ElementDef>(); else fFilter.clear(); for( int i = 0; i < elements.length; i++ ) { if( elements[i] != null ) { fFilter.put( elements[i].name(), elements[i] ); } } } } /** * Clears the filter previously set with the setFilter method */ public void clearFilter() { if( fFilter != null ) { fFilter.clear(); fFilter = null; } } private static final int EMPTY = 0, OPEN = 1, CLOSE = 2; private boolean hasContent( SIFElement o, SIFVersion version ) { if( o.getChildCount() > 0 ){ return true; } List<SimpleField> fields = o.getFields(); for( SimpleField f : fields ) { // TODO: This is a perfect place to optimize. Version-specific lookups // should be optimized if( f.fElementDef.isSupported( version ) && !f.fElementDef.isAttribute( version ) ){ return true; } } return false; } /** * Write a SIF Message in the version of SIF in effect for that object. * To change the version of SIF that is used, call the * <code>SIFMessagePayload.setSIFVersion</code> method prior to calling * this function.<p> * * @param o The SIF Message to write to the output stream */ public synchronized void write( SIFMessagePayload o ) { setSIFVersion( o.getSIFVersion() ); fWriter.write("<SIF_Message "); writeRootAttributes( true ); fWriter.println( ">" ); fWriter.indent( 1 ); writeElement((SIFElement)o); fWriter.println("</SIF_Message>"); } /** * Write a SIF Data Object in the version of SIF in effect for that object. * To change the version of SIF that is used, call the * <code>SIFDataObject.setSIFVersion</code> method prior to calling * this function.<p> * * @param o The SIFDataObject instance to write to the output stream */ public synchronized void write( SIFDataObject o ) { setSIFVersion( o.getSIFVersion() ); nsTag = o.getObjectTag(); if( o instanceof SIFDataObjectXML ){ fWriter.write(o.toXML()); } else{ writeElement((SIFElement)o); } } /** * Write a SIF Data Object to the output stream using whatever XML content * is currently defined for that object.<p> * * @param o The SIFDataObjectXML instance to write to the output stream */ public synchronized void write( SIFDataObjectXML o ) { setSIFVersion( o.getSIFVersion() ); fWriter.write(o.toXML()); } /** * Write a SIF element in the version of SIF specified.<p> * * @param version The version of SIF to use when rendering the SIF element * @param o The SIF Element instance to write to the output stream */ public synchronized void write( SIFVersion version, SIFElement o ) { setSIFVersion( version ); writeElement(o); } /** * Write a SIF element in the version of SIF currently in effect for this * SIFWriter. * <p> * * @param o The SIF Element instance to write to the output stream */ public void write( SIFElement o ) { writeElement( o ); } private void writeSIFExtendedElement(SIF_ExtendedElement element) { fWriter.write(element.getXML()); } /** * @param element * @param isLegacy if true, this method assumes that it needs to do more work, * such as looking for rendering surrogates for the specific version of SIF */ private void writeElement( SIFElement element, boolean isLegacy ) { ElementDef def = element.getElementDef(); if( !( include(element) && def.isSupported( fVersion ) ) ){ return; } if( isLegacy ){ RenderSurrogate surrogate = def.getVersionInfo( fVersion ).getSurrogate(); if( surrogate != null ){ try { surrogate.renderRaw( fWriter, fVersion, element, fFormatter ); } catch( ADKException ex ){ throw new RuntimeException( ex.getMessage(), ex ); } return; } } if( element.isEmpty() || !hasContent( element, fVersion ) ) // TODO: We need to review this special case code and determine if there // is a more efficient way to deal with XML content // Special case: XMLData if( element instanceof XMLData ){ write(element,OPEN, isLegacy); fWriter.write((( XMLData )element).getXML() ); write(element,CLOSE, isLegacy ); } else if ( element instanceof SIF_ExtendedElement && !isLegacy ) { writeSIFExtendedElement((SIF_ExtendedElement )element); } else { write(element,EMPTY, isLegacy ); } else if ( element instanceof SIF_ExtendedElement && !isLegacy ) { writeSIFExtendedElement((SIF_ExtendedElement )element); } else { write(element,OPEN, isLegacy); if( !fRootAttributesWritten ){ writeRootAttributes( false ); } List<Element> elements = fFormatter.getContent( element, fVersion ); for( Element e : elements ){ if( e instanceof SIFElement ) writeElement((SIFElement)e, isLegacy ); else write((SimpleField)e, isLegacy); } write(element,CLOSE, isLegacy); } } /** * Write a SIF element in the version of SIF currently in effect for this * SIFWriter. * @param o */ private void writeElement( SIFElement o ) { writeElement( o, fVersion.compareTo( SIFVersion.SIF20 ) < 0 ) ; } protected void write( SimpleField f, boolean isLegacy ) { if( !include(f) ){ return; } if( isLegacy ){ RenderSurrogate surrogate = f.getElementDef().getVersionInfo( fVersion ).getSurrogate(); if( surrogate != null ){ try { surrogate.renderRaw( fWriter, fVersion, f, fFormatter ); } catch( ADKException ex ){ throw new RuntimeException( ex.getMessage(), ex ); } return; } } // "<tag [attr...]>[text]" or "<tag [attr...]/>" fWriter.tab(); StringBuilder b = new StringBuilder(); b.append('<'); b.append(f.fElementDef.tag(fVersion)); String fieldValue = null; SIFSimpleType simpleValue = f.getSIFValue(); if( simpleValue != null ){ fieldValue = simpleValue.toString( fFormatter ); } // Check the value to see if it's null if( fieldValue == null ){ if( !isLegacy ){ b.append(' '); b.append( XSI_NIL +"=\"true\" />" ); fWriter.println(b.toString()); return; } else { // The specified version of SIF doesn't support // the xsi:nil attribute. Set the value to an // empty string fieldValue = ""; } } b.append('>'); if( f.isDoNotEncode() ) { b.append( fieldValue ); } else { b.append( fWriter.xmlEncode( fieldValue ) ); } b.append("</"); b.append(f.fElementDef.tag(fVersion)); b.append('>'); fWriter.println(b.toString()); } protected void write( SIFElement o, int mode, boolean isLegacy ) { String tag = o.getElementDef().tag(fVersion); if( mode == CLOSE ) { // "</tag>" fWriter.indent(-1); fWriter.tab(); fWriter.println("</"+tag+">"); } else { // "<tag [attr...]>[text]" or "<tag [attr...]/>" fWriter.tab(); fWriter.print("<"+tag); if( !fRootAttributesWritten ){ writeRootAttributes( false ); } writeAttributes(o); if( mode == EMPTY ) fWriter.println("/>"); else { SIFSimpleType sst = o.getSIFValue(); if( sst != null ){ if( sst.getValue() == null ){ if( isLegacy ){ fWriter.print(">"); } else { fWriter.print( " " + XSI_NIL +"=\"true\">" ); fWriter.pauseTab(); } } else { fWriter.print(">"); if( o.isDoNotEncode() ) { fWriter.print( sst.toString( fFormatter ) ); } else { fWriter.printXmlText( sst.toString( fFormatter ) ); } fWriter.pauseTab(); } }else { fWriter.println( ">" ); fWriter.indent(1); } } } } /** * Write the attributes of a SIFElement to the output stream * @param o The SIFElement whose attributes are to be written */ protected void writeAttributes( SIFElement o ) { StringBuilder b = new StringBuilder(); List<SimpleField> fields = fFormatter.getFields( o, fVersion ); for( SimpleField f : fields ) { ElementVersionInfo evi = f.fElementDef.getVersionInfo( fVersion ); if( evi != null && evi.isAttribute() ) { // Null attribute values are not supported in SIF, unlike // element values, which can be represented with xs:nil @SuppressWarnings("unused") SIFSimpleType sst = f.getSIFValue(); if( sst.getValue() != null ) { b.append(' '); b.append( evi.getTag() ); b.append( "=\"" ); String fieldValue = sst.toString( fFormatter ); if( f.isDoNotEncode() ) { b.append( fieldValue ); } else { b.append( fWriter.xmlEncode( fieldValue ) ); } b.append( '\"' ); } } } fWriter.print(b.toString()); } /** * Writes the given string without doing any xml encoding * @param value */ public void writeRaw( String value ) { fWriter.print( value ); } /** * Writes the given string, after xml encoding it. * @param value */ public void write( String value ) { fWriter.printXmlText( value ); } protected boolean include( Element o ) { if( o.isChanged() ) { if( fFilter == null || o.fElementDef.isObject() ){ return true; } // If the element is in the filter list, include it if( fFilter.containsKey( o.fElementDef.name() ) ){ return true; } // If any of the element's parents are in the filter list, include it Element cur = o; while( cur != null ) { if( fFilter.containsKey( cur.fElementDef.name() ) ){ return true; } cur = cur.getParent(); } // At this point the element should not be included *unless* it is // the parent of one of the elements in the filter list. In this // case it has to be included or else that child will not be. for( Iterator it = fFilter.values().iterator(); it.hasNext(); ) { ElementDef parentDef = ((ElementDef)it.next()).getParent(); if( parentDef != null && parentDef.name().equals( o.fElementDef.name() ) ) return true; } } return false; } public void write( char[] chars ){ fWriter.write( chars ); } public void flush() { fWriter.flush(); } public void close() { fWriter.close(); } private void setSIFVersion( SIFVersion version ) { fVersion = version; fFormatter = ADK.DTD().getFormatter( version ); } /** * By Default, SIFWriter writes an XML Namespace when it starts * writing to an element stream. If this is not desirable, it can * be suppressed with this call * @param suppress */ public void suppressNamespace( boolean suppress ) { fRootAttributesWritten = suppress; } private void writeRootAttributes( boolean includeVersion ) { if( !fRootAttributesWritten ) { fWriter.write(" xmlns=\"" + fVersion.getXmlns() + "\"" ); if( fFormatter.supportsNamespaces() ){ fWriter.write( " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""); } if( includeVersion ){ fWriter.write( " Version=\"" + fVersion.toString() + "\"" ); } if(nsTag != null && nsTag.startsWith("sif3")){ fWriter.write(" xmlns:sif3=\"urn:sif3placeholder\""); } } fRootAttributesWritten = true; } }