//
// Copyright (c)1998-2011 Pearson Education, Inc. or its affiliate(s).
// All rights reserved.
//
package openadk.library.services.impl;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.OutputStream;
import openadk.library.ADK;
import openadk.library.ADKException;
import openadk.library.ElementDef;
import openadk.library.Query;
import openadk.library.SIFDataObject;
import openadk.library.SIFElement;
import openadk.library.SIFVersion;
import openadk.library.SIFWriter;
import openadk.library.Zone;
import openadk.library.common.YesNo;
import openadk.library.impl.ResponseDelivery;
import openadk.library.impl.SIFIOFormatter;
import openadk.library.impl.ZoneImpl;
import openadk.library.infra.SIF_Error;
import openadk.library.services.ServiceOutputInfo;
import openadk.util.ADKStringUtils;
/*
* A hack
*/
public class ServiceOutputFileStream extends ServiceOutputStreamImpl {
protected File fFile;
protected Query fQuery;
protected ElementDef[] fQueryRestrictions;
protected String fWorkDir;
protected int fMaxSize;
protected int fCurSize;
protected int fEnvSize;
protected int fCurPacket = 0;
protected SIF_Error fError;
protected OutputStream fOutputStream;
protected Zone fZone;
protected SIFVersion fRenderAsVersion;
protected boolean fDeferResponses;
protected ServiceOutputInfo serviceOutputInfo;
/**
* The Query filter used to filter data. If set, each call to write() makes an evaluation
* of the data based on this filter. It the data does not meet the conditions of the query,
* the object is not written to the output stream.
*/
private Query fFilter;
/**
* This field is set to true in certain cases by SIFResposeSender. If it is set to true,
* the final packet that is written by this class will have the SIF_MorePackets flag set to 'Yes'
*/
private boolean fMorePackets = false;
/**
* Initialize the output stream. This method must be called after creating
* a new instance of this class and before writing any SIFDataObjects to
* the stream.
*
* @param zone The Zone associated with messages that will be written to the stream
* @param query The Query restrictions that were specified in the SIF_Request message
* @param requestSourceId The SourceId of the associated SIF_Request message
* @param requestMsgId The MsgId of the associated SIF_Request message
*
* @param requestSIFVersion The version of the SIF_Message envelope of the
* SIF_Request message (if specified and different than the SIF_Message
* version, the SIF_Request/SIF_Version element takes precedence).
* SIF_Responses will be encapsulated in a message envelope matching
* this version and SIFDataObject contents will be rendered in this
* version
*
* @param maxSize The maximum size of rendered SIFDataObject that will be
* accepted by this stream. If a SIFDataObject is written to the stream
* and its size exceeds this value after rendering the object to an XML
* stream, an ObjectTooLargeException will be thrown by the <i>write</i>
* method
*/
@Override
public void initialize(
Zone zone,
Query query,
String requestSourceId,
String requestMsgId,
SIFVersion requestSIFVersion,
int maxSize )
throws ADKException
{
fQuery = query;
initialize( zone, query == null ? null : query.getFieldRestrictions(), requestSourceId, requestMsgId, requestSIFVersion, maxSize );
}
/**
* Initialize the output stream. This method must be called after creating
* a new instance of this class and before writing any SIFDataObjects to
* the stream.
*
* @param zone The Zone associated with messages that will be written to the stream
* @param queryRestrictions The Query restrictions that were specified in the SIF_Request message
* @param requestSourceId The SourceId of the associated SIF_Request message
* @param requestMsgId The MsgId of the associated SIF_Request message
*
* @param requestSIFVersion The version of the SIF_Message envelope of the
* SIF_Request message (if specified and different than the SIF_Message
* version, the SIF_Request/SIF_Version element takes precedence).
* SIF_Responses will be encapsulated in a message envelope matching
* this version and SIFDataObject contents will be rendered in this
* version
*
* @param maxSize The maximum size of rendered SIFDataObject that will be
* accepted by this stream. If a SIFDataObject is written to the stream
* and its size exceeds this value after rendering the object to an XML
* stream, an ObjectTooLargeException will be thrown by the <i>write</i>
* method
*/
@Override
public void initialize(
Zone zone,
ElementDef[] queryRestrictions,
String requestSourceId,
String requestMsgId,
SIFVersion requestSIFVersion,
int maxSize )
throws ADKException
{
fZone = zone;
fQueryRestrictions = queryRestrictions;
fReqId = requestMsgId;
fDestId = requestSourceId;
fMaxSize = maxSize;
fCurPacket = 0;
fRenderAsVersion = requestSIFVersion;
//
// Messages written to this stream are stored in the directory
// "%adk.home%/work/%zoneId%_%zoneHost%/responses". One or more files
// are written to this directory, where each file has the name
// "destId.requestId.{packet}.pkt". As messages are written to the
// stream, the maxSize property is checked to determine if the size of
// the current file will be larger than the maxSize. If so, the file is
// closed and the packet number incremented. A new file is then created
// for the message and all subsequent messages until maxSize is again
// exceeded.
//
StringBuffer workDir = new StringBuffer();
workDir.append( zone.getAgent().getHomeDir() );
if( workDir.charAt( workDir.length() - 1 ) != File.separatorChar )
workDir.append( File.separatorChar );
workDir.append( "work" );
workDir.append( File.separator );
workDir.append( ADKStringUtils.safePathString( zone.getZoneId() + "_" + zone.getZoneUrl().getHost() ) );
workDir.append( File.separator );
workDir.append( "responses" );
fWorkDir = workDir.toString();
// Ensure work directory exists
File dir = new File(fWorkDir);
dir.mkdirs();
// Get the size of the SIF_Message envelope to determine the actual
// packet size we're producing
fEnvSize = this.calcEnvelopeSize((ZoneImpl)fZone);
}
/**
* Start writing messages to a new packet file. The current packet file
* stream is closed, the packet number incremented by one, and a new packet
* file created. All subsequent calls to the <i>write</i> method will render
* messages to the newly-created packet file.
*/
protected void newPacket()
throws IOException
{
close();
fCurPacket++;
fCurSize = fEnvSize;
// Create output file and stream
fFile = createOutputFile();
if( fOutputStream != null ){
fOutputStream.close();
}
fOutputStream = new FileOutputStream( fFile );
}
/**
* Create a File descriptor of the current output file
*/
protected File createOutputFile()
throws IOException
{
StringBuilder builder = new StringBuilder();
builder.append( fWorkDir );
builder.append( File.separator );
ServiceResponseDelivery.serializeResponsePacketFileName( builder, fDestId, fReqId, fCurPacket, fRenderAsVersion, (fError != null ) );
return new File( builder.toString() );
}
/**
* Called when the Publisher.onQuery method has thrown a SIFException,
* indicating an error should be returned in the SIF_Response body
*/
@Override
public void setError( SIF_Error error )
throws ADKException
{
fError = error;
//
// Write a SIF_Response packet that contains only this SIF_Error
//
ByteArrayOutputStream buffer = null;
try
{
newPacket();
// Write to memory stream first
buffer = new ByteArrayOutputStream();
SIFWriter out = new SIFWriter( buffer,fZone );
out.suppressNamespace( true );
out.write( fRenderAsVersion, error );
out.flush();
out.close();
// Write to current packet
buffer.writeTo( fOutputStream );
fCurSize += buffer.size();
}
catch( IOException ioe )
{
throw new ADKException("Failed to write Publisher SIF_Error data (packet "+fCurPacket+") to "+fFile.getAbsolutePath()+": " + ioe, fZone );
}
finally
{
if( buffer != null ){
try
{
buffer.close();
}catch( IOException unexpectedError ){
fZone.getLog().warn( "Unexpected Error closing output stream: " + unexpectedError.getMessage(), unexpectedError );
}
}
}
}
@Override
public void commit()
throws ADKException
{
try
{
if( fDeferResponses )
{
abort();
}
else
{
// If no objects or SIF_Errors have been written to the stream, we still
// need to return an empty SIF_Response to the ZIS.
if( fOutputStream == null ) {
try {
newPacket();
close();
} catch( IOException ioe ) {
throw new ADKException( "Could not commit the stream because of an IO error writing an empty SIF_Response packet: " + ioe, fZone );
}
}
String responseFileName = ServiceResponseDelivery.serializeResponseHeaderFileName( fDestId, fReqId, fMorePackets );
// Write out "destId.requestId." file to signal the Publisher has finished
// writing all responses successfully. This file will hang around until
// all "requestId.{packet}.pkt" files have been sent to the ZIS by the ADK,
// a process that could occur over several agent sessions if the agent
// is abruptly terminated.
//
String fileName = fWorkDir + File.separator + responseFileName;
try {
new File( fileName ).createNewFile();
} catch( IOException ioe ) {
fZone.getLog().warn( "Unable to create SIF_Response header file: " + fileName + ". " + ioe.getMessage(), ioe );
}
// Process response packets
ServiceResponseDelivery serviceResponseDelivery = ((ZoneImpl)fZone).getServiceResponseDelivery();
serviceResponseDelivery.setService(serviceOutputInfo.getService());
serviceResponseDelivery.setOperation(serviceOutputInfo.getOperation());
serviceResponseDelivery.setServiceMsgId(serviceOutputInfo.getSIFRequestMsgId());
serviceResponseDelivery.process();
}
}
finally
{
fZone = null;
}
}
@Override
public void abort()
throws ADKException
{
final String _filter = fDestId + "." + fReqId;
// Delete "destId.requestId.*" files
File dir = new File( fWorkDir );
File[] toDelete = dir.listFiles(
new FilenameFilter() {
public boolean accept( File file, String name ) {
return name.startsWith(_filter);
}
}
);
if( toDelete != null ) {
for( int i = 0; i < toDelete.length; i++ )
toDelete[i].delete();
}
fZone = null;
}
@Override
public void close()
throws IOException
{
if( fOutputStream != null ) {
fOutputStream.flush();
fOutputStream.close();
}
}
/**
* Defer sending SIF_Response messages and ignore any objects written to this stream.<p>
*
* See the {@link openadk.library.SIFResponseSender} class comments for
* more information about using this method.<p>
*
* @see openadk.library.SIFResponseSender
*
* @since ADK 1.5.1
*/
public void deferResponse()
throws ADKException
{
fDeferResponses = true;
}
/* (non-Javadoc)
* @see openadk.library.DataObjectOutputStream#setAutoFilter(openadk.library.Query)
*/
public void setAutoFilter(Query filter) {
fFilter = filter;
}
/* (non-Javadoc)
* @see openadk.library.impl.DataObjectOutputStreamImpl#getSIF_MorePackets()
*/
@Override
public YesNo getSIF_MorePackets() {
return fMorePackets ? YesNo.YES : YesNo.NO;
}
/* (non-Javadoc)
* @see openadk.library.impl.DataObjectOutputStreamImpl#getSIF_PacketNumber()
*/
@Override
public int getSIF_PacketNumber() {
// Special case: If newPacket() has not been called yet,
// fWriter will be null, in which case, we need to add one to fCurPacket
// to get the actual value of the packet
if( fOutputStream == null ){
return fCurPacket + 1;
} else {
return fCurPacket;
}
}
/* (non-Javadoc)
* @see openadk.library.impl.DataObjectOutputStreamImpl#setSIF_MorePackets(openadk.library.common.YesNo)
*/
@Override
public void setSIF_MorePackets(YesNo morePacketsValue) {
fMorePackets = morePacketsValue.equals( YesNo.YES );
}
/* (non-Javadoc)
* @see openadk.library.impl.DataObjectOutputStreamImpl#setSIF_PacketNumber(int)
*/
@Override
public void setSIF_PacketNumber(int packetNumber) {
// If fWriter is not initialized, set the fCurPacket value to
// 1 value less (allows it to be properly incremented in newPacket())
if( fOutputStream == null ){
fCurPacket = packetNumber - 1;
} else {
throw new IllegalStateException("Cannot set the packet number after objects have already been written" );
}
}
public ServiceOutputInfo getServiceOutputInfo() {
return serviceOutputInfo;
}
public void setServiceOutputInfo(ServiceOutputInfo serviceOutputInfo) {
this.serviceOutputInfo = serviceOutputInfo;
}
public void writeBuffer(ByteArrayOutputStream buffer) throws IOException {
if( fOutputStream == null )
newPacket();
buffer.writeTo( fOutputStream );
}
/**
* Write a SIFDataObject to the stream
*/
@Override
public void write( SIFElement data )
throws ADKException
{
// Check to see if the data object is null or if the
// deferResponses() property has been set
if( data == null || fDeferResponses ){
return;
}
// Check to see if a SIF_Error has already been written
if( fError != null ){
throw new ADKException("A SIF_Error has already been written to the stream",fZone);
}
ByteArrayOutputStream buffer = null;
try
{
if( fOutputStream == null || fZone.getProperties().getOneObjectPerResponse() ){
newPacket();
}
// Write to memory stream first so we can determine if the resulting
// message will fit in the current packet
// TODO: The mechanism below is not properly calculating packet size
// for Unicode characters. The length of the CharArray in the code below
// may be different than the number of characters written out to the UTF8
// stream by fWriter.
buffer = new ByteArrayOutputStream();
SIFWriter out = new SIFWriter( SIFIOFormatter.createOutputWriter( buffer ), fZone );
out.suppressNamespace( true );
data.setSIFVersion( fRenderAsVersion );
// TODO: Fix up
if ( fQueryRestrictions != null ){
out.setFilter( fQueryRestrictions );
}
out.write( data );
out.flush();
out.close();
if( ( buffer.size() + fCurSize ) > fMaxSize )
{
// If the current packet size is equal to the envelope size (e.g. no objects
// have been written), we have exceeded the size of the buffer and need to abort
if( fCurSize == fEnvSize ) {
String errorMessage = "Publisher result data in packet " + fCurPacket + " too large (" +
buffer.size() + " [Data] + "+fEnvSize+" [Sif Envelope] > " + fMaxSize + ")";
if( fZone.getProperties().getOneObjectPerResponse() ){
errorMessage += " [1 Object per Response Packet]";
}
throw new ADKException( errorMessage, fZone );
}
// Create new packet for this object
newPacket();
}
if( ( ADK.debug & ADK.DBG_MESSAGE_CONTENT ) != 0 ){
// Convert the binary data to a string for logging purposes
((ZoneImpl)fZone).log.debug( "Writing object to SIF_Response packet #" + fCurPacket + ":\r\n" + buffer.toString( SIFIOFormatter.CHARSET.name() ) );
}
// Write to current packet
buffer.writeTo( fOutputStream );
fCurSize += buffer.size();
}
catch( IOException ioe )
{
throw new ADKException("Failed to write Publisher result data (packet "+fCurPacket+") to "+fFile.getAbsolutePath()+": " + ioe, fZone );
}
finally
{
if( buffer != null ){
try
{
buffer.close();
} catch( IOException unexpectedError ){
fZone.getLog().warn( "Unexpected Error closing output stream: " + unexpectedError.getMessage(), unexpectedError );
}
}
}
}
public void write(SIFDataObject data) throws ADKException {
// If the autoFilter property has been set, determine if this object meets the
// conditions of the filter
if( fFilter != null ){
if( !fFilter.evaluate( data ) ){
// TODO: Perhaps this feature should log any objects not written to the output
// stream if extended logging is enabled
return;
}
}
write( data );
}
}