// // Copyright (c)1998-2011 Pearson Education, Inc. or its affiliate(s). // All rights reserved. // package openadk.library.impl; import java.io.*; import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.charset.Charset; import java.util.*; import openadk.library.*; import openadk.library.common.YesNo; import openadk.library.impl.ZoneImpl; import openadk.library.infra.*; import openadk.util.ADKStringUtils; import openadk.util.GUIDGenerator; /** * Sends prepared SIF_Response packets on behalf of a zone.<p> * * Each ZoneImpl has one instance of ResponseDelivery that it constructs prior * to connecting to the zone. ZoneImpl should call the <code>process</code> * method from an inner thread to cause the ADK to send all outstanding * SIF_Response packets leftover from previous sessions. Subsequently, ZoneImpl * should call this method each time a SIF_Request is successfully handled by * the agent's Publisher to send all pending SIF_Response packets waiting in * the work directory. (Packets are created by the DataObjectFileOutputStream * implementation.)<p> * * To determine if there are pending SIF_Responses, a directory of all files in * the zone work directory is obtained. Any files without an extension indicate * one or more SIF_Response packets exist for the SIF_Response, and the name of * the file identifies the SourceId and MsgId of the SIF_Request. For each such * file, ResponseDelivery attempts to send each of the packet files named * "{requestSourceId}.{requestMsgId}.{packetNum}.pkt". When all are sent * successfully, the "{requestSourceId}.{requestMsgId}." file is deleted. * * @author Eric Petersen * @version ADK 1.0 */ public class ResponseDelivery { /** * Identifies the generic SIF_Response input directory */ public static final byte SRC_GENERIC = (byte)0; /** * Identifies the SIF_ReportObject SIF_Response input directory */ public static final byte SRC_SIFREPORTOBJECT = (byte)1; /** * Instruct customers to set this flag to false if they want to see the * output generated by ResponseDelivery in the agent's work/responses * directory (the packet files won't be deleted when the SIF_Responses are * sent.) */ public static boolean DELETE_ON_SUCCESS = true; protected String fWorkDir; protected SIFParser fParser; protected ZoneImpl fZone; protected byte fSrc; char[] buf = new char[1024]; /** * Constructor.<p> * * @param zone The zone * * @param source Identifies the source of pending SIF_Response packets to * be processed: <code>RSPTYPE_GENERIC</code> to process files in the default * 'responses' directory, or <code>RSPTYPE_SIFREPORTOBJECT</code> to process * SIF_ReportObject files in the 'responses/reports' directory */ public ResponseDelivery( Zone zone, byte source ) throws ADKException { fZone = (ZoneImpl)zone; fParser = SIFParser.newInstance(); fSrc = source; fWorkDir = getSourceDirectory( source, fZone ); } /** * Determines the full path to the source directory.<p> * * All SIF_Response source directories are located in the agent's work directory. * Generic SIF_Responses are found in a directory named "{agent-home}/work/{zoneId}_{zoneHost}/responses/". * SIF_Responses for SIF_ReportObject requests are found in a directory named * "{agent-home}/work/{zoneId}_{zoneHost}/responses/reports".<p> * * @param zone The associated zone * @param source Identifies the type of pending SIF_Response packets. This flag may * be any <code>SRC_</code> constant defined by this class. * @return The fully-qualified path to the directory where pending response packets * are located for the type of responses identified by <i>source</i> */ public static String getSourceDirectory( byte source, Zone zone ) { 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" ); if( source == SRC_SIFREPORTOBJECT ) { workDir.append( File.separator ); workDir.append( "reports" ); } return workDir.toString(); } /** * Determines if the specified source directory exists and contains one or more files.<p> * @param source Identifies the type of pending SIF_Response packets. This flag may * be any <code>SRC_</code> constant defined by this class. * @return <code>true</code> if the source directory exists and contains at least one file */ public static boolean hasPendingPackets( byte source, Zone zone ) { File f = new File( getSourceDirectory( source, zone ) ); if( f.exists() ) { File[] contents = f.listFiles(); if( contents != null && contents.length > 0 ) return true; } return false; } /** * Signal the ResponseDelivery thread that SIF_Response packets are available * for sending to the zone. */ public synchronized void process() throws ADKException { // Look for all non-"*.pkt" files; if a file doesn't end in an extension, // it has no content and represents a pending SIF_Response. These are the // files we're interested in processing in the files[] loop below. File dir = new File( fWorkDir ); File[] files = dir.listFiles( new FilenameFilter() { public boolean accept( File dir, String name ) { boolean isCandidate = !name.endsWith(".pkt") && // SIF_Response packet files !name.endsWith(".rpt") && // ReportInfo files for SIF_ReportObject responses !name.startsWith("."); // hidden files on some platforms like Mac OS X if( isCandidate ){ String fullName = dir.getPath() + "\\" + name; File candidateFile = new File( fullName ); isCandidate = !candidateFile.isDirectory(); } return isCandidate; } } ); if( files != null && files.length > 0 ) { if( ( ( ADK.debug & ADK.DBG_MESSAGING_RESPONSE_PROCESSING ) != 0 ) ) fZone.log.debug( "Processing " + ( files.length ) + " pending SIF_Response packets..." ); for( int i = 0; i < files.length; i++ ) { // Todo : parse off the more packet flag String fileName = files[i].getName(); boolean responseHasMorePackets = fileName.endsWith( "Y" ); final String _msgId = fileName.substring( 0, fileName.length() - 2 ); // Get all packets awaiting delivery... File[] packets = dir.listFiles( new FilenameFilter() { public boolean accept( File dir, String name ) { return name.startsWith(_msgId) && name.endsWith(".pkt"); } } ); if( packets == null ) continue; if( ( ADK.debug & ADK.DBG_MESSAGING_RESPONSE_PROCESSING ) != 0 ) fZone.log.debug( "Found " + ( packets.length ) + " pending SIF_Response packets for request " + _msgId ); // Sort the files by packet # Arrays.sort( packets, new Comparator() { public int compare( Object o1, Object o2 ) { // Get first file's packet # StringTokenizer tok = new StringTokenizer( ((File)o1).getName(),"." ); tok.nextToken(); tok.nextToken(); int n1 = Integer.parseInt( tok.nextToken() ); // Get second file's packet # tok = new StringTokenizer( ((File)o2).getName(),"." ); tok.nextToken(); tok.nextToken(); int n2 = Integer.parseInt( tok.nextToken() ); if( n1 < n2 ) return -1; else if( n1 == n2 ) return 0; return 1; } public boolean equals( Object obj ) { return this.equals(obj); } } ); // Process each packet int p = 0; for( p = 0; p < packets.length; p++ ){ boolean morePackets = responseHasMorePackets || ( p < ( packets.length - 1 ) ) ; sendPacket( packets[p], morePackets ); } // Delete files[i] if we processed all packets. If the // thread is being shut down and not all packets were // processed, however, leave the file for the next // session. if( p >= packets.length ) { // System.out.println( "Deleting " + files[i].getAbsolutePath() ); files[i].delete(); if( fSrc == SRC_SIFREPORTOBJECT ) { // SRC_SIFREPORTOBJECT: Also delete the .rpt file File f = new File( fWorkDir + File.separator + _msgId + ".rpt" ); // System.out.println( "Deleting " + f.getAbsolutePath() ); f.delete(); } } } } } protected void sendPacket( File file, boolean morePackets ) throws ADKException { long extraBytes = 0; ResponsePacketInfo responsePacket = deserializeResponseFileName( file.getName() ); /* If we're processing SIF_ReportObject responses, read the ReportInfo * data from the "requestMsgId.rpt" file * * TT 894 - If a SIFException is thrown after the ReportInfo is set on * the ReportObjectStream, then we don't want to include that ReportInfo * in the packet with the error. In that case, rptInfoReader will be * null and will not be included in the list of payloads below. */ BufferedReader rptInfoReader = null; if( fSrc == SRC_SIFREPORTOBJECT && !responsePacket.errorPacket ) { try { File f = new File( fWorkDir + File.separator + responsePacket.destinationId + "." + responsePacket.requestMsgId + ".rpt" ); rptInfoReader = SIFIOFormatter.createInputReader( new FileInputStream( f ) ); extraBytes = f.length(); } catch( IOException fnfe ) { fZone.log.debug( "Error sending SIF_ReportObject packet #" + responsePacket.packetNumber + ( morePackets ? "": " (last packet)" ) + ", file not found: " + fnfe.getMessage() ); } } if( ( ADK.debug & ADK.DBG_MESSAGING ) != 0 ) fZone.log.debug( "Sending " + ( responsePacket.errorPacket ? "SIF_Error response" : "SIF_Response" ) + " packet #" + responsePacket.packetNumber + ( morePackets ? "" : " (last packet)" ) ); // Prepare SIF_Response SIF_Response rsp = new SIF_Response(); rsp.setSIF_MorePackets( morePackets ? YesNo.YES : YesNo.NO ); rsp.setSIF_RequestMsgId( responsePacket.requestMsgId ); rsp.setSIF_PacketNumber( responsePacket.packetNumber ); // The SIF_Response is rendered in the same version of SIF as the original SIF_Request rsp.setSIFVersion( responsePacket.version ); if( responsePacket.errorPacket ) { // Write an empty "<SIF_Error> </SIF_Error>" for the MessageStreamer // to replace SIF_Error err = new SIF_Error(); err.setTextValue( " " ); rsp.setSIF_Error( err ); } if ( !responsePacket.errorPacket || responsePacket.version.getMajor() == 1 ) { // Write an empty "<SIF_ObjectData> </SIFObjectData>" for the // MessageStreamer to fill in. If this is an errorPacket, the empty // element is required per the SIF 1.x Specifications, but disallowed // in SIF 2.x. SIF_ObjectData placeholder = new SIF_ObjectData(); placeholder.setTextValue( " " ); rsp.setSIF_ObjectData( placeholder ); } // Assign values to message header - this is usually done by // MessageDispatcher.send() but because we're preparing a SIF_Response // output stream we need to do it manually SIF_Header hdr = rsp.getHeader(); hdr.setSIF_Timestamp( Calendar.getInstance() ); hdr.setSIF_MsgId(GUIDGenerator.makeGUID()); hdr.setSIF_SourceId(fZone.getAgent().getId()); hdr.setSIF_Security(fZone.getFDispatcher().secureChannel()); hdr.setSIF_DestinationId( responsePacket.destinationId ); // Write SIF_Response -- without its SIF_ObjectData payload -- to a buffer ByteArrayOutputStream bytes = new ByteArrayOutputStream(); SIFWriter writer = new SIFWriter( SIFIOFormatter.createOutputWriter( bytes ), false ); writer.write( rsp ); writer.flush(); writer.close(); // Determine the total number of bytes we'll be sending int a = bytes.size(); long b = file.length(); try { // Send the SIF_Response as a stream Reader envelope = SIFIOFormatter.createInputReader( new ByteArrayInputStream( bytes.toByteArray() ) ); BufferedReader fr = SIFIOFormatter.createInputReader( new FileInputStream( file ) ); Reader[] payloads = null; if( fSrc == SRC_GENERIC ) { payloads = new BufferedReader[] { new BufferedReader(fr) }; } else { if( rptInfoReader != null ) payloads = new BufferedReader[] { rptInfoReader, fr }; else payloads = new BufferedReader[] { fr }; } MessageStreamer ms = new MessageStreamer( envelope, payloads, responsePacket.errorPacket ? "<SIF_Error>" : "<SIF_ObjectData>" ); if( responsePacket.errorPacket ){ ms.setReplaceMode( true ); } if( ( ADK.debug & ADK.DBG_MESSAGING ) != 0 ) fZone.log.debug("Send SIF_Response"); if( ( ADK.debug & ADK.DBG_MESSAGING_DETAILED ) != 0 ) { fZone.log.debug(" MsgId: " + rsp.getMsgId() ); } String ackStr = fZone.fProtocolHandler.send( ms, a+(int)b+(int)extraBytes ); // Parse the results into a SIF_Ack SIF_Ack ack = (SIF_Ack)fParser.parse(ackStr,fZone); if( ack != null ) { ack.LogRecv(fZone.log); } // If we get here, the message was sent successfully envelope.close(); for( int i = 0; i < payloads.length; i++ ) payloads[i].close(); fr.close(); if( DELETE_ON_SUCCESS ) file.delete(); } catch( ADKException adke ) { ADKUtils._throw( adke, fZone.log ); } catch( Exception e ) { ADKUtils._throw( new ADKException("Failed to send SIF_Response: " + e, fZone, e ), fZone.log ); } } /** * This method is implemented here to be in close proximity to the method that parses a file name to * retrieve these bits of information back out * @param builder * @param destinationId * @param requestMsgId * @param packetNumber * @param renderAsVersion * @param errorPacket */ static void serializeResponsePacketFileName( StringBuilder builder, String destinationId, String requestMsgId, int packetNumber, SIFVersion renderAsVersion, boolean errorPacket ){ // FORMAT: "{requestSourceId}.{requestMsgId}.{packet#}.{ver}[.$].pkt" builder.append( serializeToken(destinationId) ); builder.append( '.' ); builder.append( requestMsgId ); builder.append( '.' ); builder.append( packetNumber ); builder.append( '.' ); builder.append( renderAsVersion.toSymbol() ); if( errorPacket ){ builder.append( ".$" ); } builder.append( ".pkt" ); } /** * Takes a token component and makes sure that there are no periods in the name, * and safely encodes it into a filename * @param destinationId * @return */ @SuppressWarnings("deprecation") private static String serializeToken(String destinationId) { destinationId = destinationId.replace( ".", "~~" ); try { destinationId = URLEncoder.encode( destinationId, SIFIOFormatter.CHARSET_UTF8 ); } catch (UnsupportedEncodingException e) { e.printStackTrace(); destinationId = URLEncoder.encode( destinationId ); } return destinationId; } @SuppressWarnings("deprecation") private static String deserializeToken(String destinationId ) { try { destinationId = URLDecoder.decode( destinationId, SIFIOFormatter.CHARSET_UTF8 ); } catch (UnsupportedEncodingException e) { e.printStackTrace(); destinationId = URLDecoder.decode( destinationId ); } destinationId = destinationId.replace( "~~", "." ); return destinationId; } static String serializeResponseHeaderFileName( String destinationId, String requestMsgId, boolean morePackets ) { return serializeToken(destinationId) + '.' + requestMsgId + '.' + (morePackets ? 'Y' : 'N'); } private static ResponsePacketInfo deserializeResponseFileName( String shortFileName ){ ResponsePacketInfo responsePacket = new ResponsePacketInfo(); // Get SIF_DestinationId, SIF_RequestMsgId, and SIF_PacketNumber from // the filename // FORMAT: "{requestSourceId}.{requestMsgId}.{packet#}.{ver}.{morePackets}[.$].pkt" StringTokenizer tok = new StringTokenizer( shortFileName,"."); // TODO: Fix serialization of sourceIds into filename String destId = tok.nextToken(); destId = deserializeToken( destId ); responsePacket.destinationId = destId; responsePacket.requestMsgId = tok.nextToken(); responsePacket.packetNumber = Integer.parseInt( tok.nextToken() ); String ver = tok.nextToken(); ver = ver.replace('_','.'); responsePacket.version = SIFVersion.parse( ver ); // A leading $ on the filename indicates this is a response packet // with a SIF_Error responsePacket.errorPacket = shortFileName.endsWith("$.pkt"); return responsePacket; } private static class ResponsePacketInfo { public boolean errorPacket = false; public String destinationId; public String requestMsgId; public int packetNumber; public SIFVersion version; } }