// // Copyright (c)1998-2011 Pearson Education, Inc. or its affiliate(s). // All rights reserved. // package openadk.library; import openadk.library.infra.*; import org.xml.sax.InputSource; import java.io.*; import java.util.Calendar; import java.util.HashMap; import java.util.List; /** * Encapsulates information about a <code>SIF_Message</code>, including its * payload type, header fields, and raw XML message content.<p> * * An instance of this class is passed to the <i>MessageInfo</i> parameter of * all ADK message handlers like Subscriber.onEvent, Publisher.onQuery, and * QueryResults.onQueryResults so that implementations of those methods can * access header fields or XML content associated with an incoming message. * Callers should cast the <i>MessageInfo</i> object to a SIFMessageInfo type * in order to call the methods of this class that are specific to the Schools * Interoperability Framework. * <p> * * Note that raw XML content is only retained if the "<code>adk.messaging.keepMessageContent</code>" * agent property is enabled. Otherwise, the <code>getMessage</code> method * returns a <code>null</code> value. Refer to the AgentProperties class for a * description of all agent and zone properties. * <p> * * To use SIFMessageInfo, cast the <i>MessageInfo</i> parameter as shown below. * <p> * * <code> *     public void onEvent( Event event, Zone zone, MessageInfo info )<br/> *     {<br/> *         SIFMessageInfo inf = (SIFMessageInfo)info;<br/> *         String sourceId = inf.getSourceId();<br/> *         String msgId = inf.getMsgId();<br/> *         SIFVersion version = inf.getSIFVersion();<br/><br/> *         // Display some information about this SIF_Event...<br/> *         System.out.println( "SIF_Event message with ID " + msgId + <br/> *             " received from agent " + sourceId + <br/> *             " in zone " + zone.getZoneId() + "."<br/> *             " This is a SIF " + version.toString() + " message." );<br/> * <br/> *         ...<br/> * </code> * </p> * * @author Eric Petersen * @version ADK 1.0 */ public class SIFMessageInfo implements MessageInfo { // TODO: Determine if we should use a single HashMap or not.... For now, // splitting them out for clarity private HashMap<String, String> fAttr = new HashMap<String, String>(); private HashMap<String, Object> fObjects = new HashMap<String, Object>(); private SIFContext[] fContexts; protected Zone fZone; protected byte fPayload; protected String fMessage; // TODO: fHeader should be removed from this class for consistency in the APIs. All // header values should be stored in the fAttr Map protected SIF_Header fHeader; protected SIFVersion fPayloadVersion; /** * Called by the ADK to construct a SIFMessageInfo instance */ public SIFMessageInfo() { super(); } /** * Called by the ADK to construct a SIFMessageInfo * @param msg The SIF_Message * @param zone The associated zone */ public SIFMessageInfo( SIFMessagePayload msg, Zone zone ) { super(); fZone = zone; fPayload = ADK.DTD().getElementType( msg.fElementDef.name() ); if( zone.getProperties().getKeepMessageContent() ) { try { StringWriter sw = new StringWriter(); SIFWriter out = new SIFWriter( sw, zone ); out.write( msg ); out.flush(); out.close(); sw.close(); fMessage = sw.getBuffer().toString(); } catch( Exception ex ) { } } // Set SIF_Header values fHeader = msg.getHeader(); // Get the list of contexts for this message try { List<SIFContext> contexts = msg.getSIFContexts(); fContexts = new SIFContext[ contexts.size() ]; contexts.toArray( fContexts ); } catch (ADKNotSupportedException contextNotSupported ){ // TODO: Determine if we should be throwing here or what... throw new IllegalArgumentException( "One or more SIF Contexts are not supported: " + contextNotSupported.getMessage(), contextNotSupported ); } // Set information about the message payload fPayloadVersion = msg.getSIFVersion(); // JEN SIF_ServiceInput if (msg instanceof SIF_ServiceInput) { SIF_ServiceInput req = (SIF_ServiceInput)msg; fObjects.put( "SIF_MaxBufferSize", req.getSIF_MaxBufferSize() ); fObjects.put( "SIF_Service", req.getSIF_Service() ); fObjects.put( "SIF_Operation", req.getSIF_Operation() ); fObjects.put( "SIF_ServiceMsgId", req.getSIF_ServiceMsgId() ); fObjects.put( "SIF_PacketNumber", req.getSIF_PacketNumber() ); setAttribute( "SIF_MorePackets", req.getSIF_MorePackets() ); } else if (msg instanceof SIF_ServiceOutput) { SIF_ServiceOutput req = (SIF_ServiceOutput)msg; // fObjects.put( "SIF_MaxBufferSize", req.getSIF_MaxBufferSize() ); fObjects.put( "SIF_Service", req.getSIF_Service() ); fObjects.put( "SIF_Operation", req.getSIF_Operation() ); fObjects.put( "SIF_ServiceMsgId", req.getSIF_ServiceMsgId() ); fObjects.put( "SIF_PacketNumber", req.getSIF_PacketNumber() ); setAttribute( "SIF_MorePackets", req.getSIF_MorePackets() ); } else if (msg instanceof SIF_ServiceNotify) { SIF_ServiceNotify req = (SIF_ServiceNotify)msg; // fObjects.put( "SIF_MaxBufferSize", req.getSIF_MaxBufferSize() ); fObjects.put( "SIF_Service", req.getSIF_Service() ); fObjects.put( "SIF_Operation", req.getSIF_Operation() ); fObjects.put( "SIF_ServiceMsgId", req.getSIF_ServiceMsgId() ); fObjects.put( "SIF_PacketNumber", req.getSIF_PacketNumber() ); setAttribute( "SIF_MorePackets", req.getSIF_MorePackets() ); } switch( fPayload ) { case SIFDTD.MSGTYP_REQUEST: { SIF_Request req = (SIF_Request)msg; fObjects.put( "SIF_MaxBufferSize", req.getSIF_MaxBufferSize() ); fObjects.put( "SIF_RequestVersions", req.parseRequestVersions( fZone.getLog() ) ); } break; case SIFDTD.MSGTYP_RESPONSE: { SIF_Response rsp = (SIF_Response)msg; setSIFRequestMsgId( rsp.getSIF_RequestMsgId() ); fObjects.put( "SIF_PacketNumber", rsp.getSIF_PacketNumber() ); setAttribute( "SIF_MorePackets", rsp.getSIF_MorePackets() ); } break; } } /** * Assigns a SIF_Header to this message info. The SIF_Header element is usually * extracted from the message by the constructor. When the default constructor * is called, SIF_Header can be assigned separately by calling this method. Note * this method is intended to be called internally by the class framework.<p> * * @param header a SIF_Header instance * * @see #getSIFHeader */ public void setSIFHeader( SIF_Header header ) { // TODO: fHeader should be removed from this class for consistency in the APIs. All // header values should be stored in the fAttr Map fHeader = header; } /** * Gets the SIF_Header encapsulated by this object.<p> * @return The SIF_Header instance extracted from the message passed to * the constructor or assigned via the <code>setSIFHeader</code> method. * @see #setSIFHeader */ public SIF_Header getSIFHeader() { return fHeader; } /** * Gets the zone from which the message originated.<p> * @return The Zone instance from which the message originated */ public Zone getZone() { return fZone; } /** * Gets the SIF payload message type.<p> * @return A <code>MSGTYP_</code> constant from the SIFDTD class * (e.g. <code>SIFDTD.MSGTYPE_REQUEST</code>) */ public byte getPayloadType() { return fPayload; } /** * Gets the SIF payload message element tag.<p> * @return The element tag of the message (e.g. "SIF_Request") */ public String getPayloadTag() { return ADK.DTD().getElementTag(fPayload); } /** * Gets the SIF_Message header timestamp.<p> * @return The <code>SIF_Header/SIF_Date</code> and <code>SIF_Header/SIF_Time</code> * element values as a Date instance, identifying the time and date the * message was sent */ public Calendar getTimestamp() { return fHeader.getSIF_Timestamp(); } /** * Gets the value of the <code>SIF_MsgId</code> header element * @return The value of the <code>SIF_Header/SIF_MsgId</code> element, the * unique GUID assigned to the message by its sender */ public String getMsgId() { return fHeader.getSIF_MsgId(); } /** * Gets the value of the <code>SIF_SourceId</code> header element<p> * @return The value of the <code>SIF_Header/SIF_SourceId</code> element, * which identifies the agent that originated the message */ public String getSourceId() { return fHeader.getSIF_SourceId(); } /** * Gets the value of the <code>SIF_DestinationId</code> header element<p> * @return The value of the optional <code>SIF_Header/SIF_SourceId</code> element. * When present, it identifies the agent to which the message should be routed * by the zone integration server. */ public String getDestinationId() { return fHeader.getSIF_DestinationId(); } /** * Gets the value of the optional <code>SIF_Security/SIF_SecureChannel/SIF_AuthenticationLevel</code> header element<p> * @return The authentication level or zero if not specified */ public int getAuthenticationLevel() { try { return Integer.parseInt( fHeader.getSIF_Security().getSIF_SecureChannel().getSIF_AuthenticationLevel() ); } catch( Throwable thr ) { return 0; } } /** * Gets the value of the optional <code>SIF_Security/SIF_SecureChannel/SIF_EncryptionLevel</code> header element<p> * @return The encryption level or zero if not specified */ public int getEncryptionLevel() { try { return Integer.parseInt( fHeader.getSIF_Security().getSIF_SecureChannel().getSIF_EncryptionLevel() ); } catch( Throwable thr ) { return 0; } } /** * Gets the content of the raw XML message<p> * @return The raw XML message content as it was received by the ADK. If * the <code>adk.keepMessageContent</code> agent or zone property has * a value of "false" (the default), null is returned. */ public String getMessage() { return fMessage; } /** * Gets the version of SIF associated with the message. The version is * determined by inspecting the <i>xmlns</i> attribute of the SIF_Message * envelope.<p> * * @return A SIFVersion object identifying the version of SIF associated * with the message * * @see #getLatestSIFRequestVersion() */ public SIFVersion getSIFVersion() { return fPayloadVersion; } /** * For SIF_Response messages, sets the SIF_MsgId of the associated SIF_Request * message. This method is called internally by the class framework.<p> * @param msgId The value to assign to the <code>SIF_Header/SIF_RequestMsgId</code> * element */ public void setSIFRequestMsgId( String msgId ) { setAttribute( "SIF_RequestMsgId", msgId ); } /** * For SIF_Response messages, gets the SIF_MsgId of the associated SIF_Request.<p> * @return The value of the <code>SIF_Header/SIF_RequestMsgId</code> element, or * null if the message encapsulated by this SIFMessageInfo instance is not a * SIF_Response message */ public String getSIFRequestMsgId() { return (String)fAttr.get("SIF_RequestMsgId"); } /** * For SIF_Request messages, sets the SIF version that responses should conform to. * This method is called internally by the class framework.<p> * @param versions One or more SIFVersion instances identifying the version of the specification * that SIF_Responses should conform to. * @see #getSIFVersion */ public void setSIFRequestVersion( SIFVersion... versions ) { fObjects.put("SIF_RequestVersions", versions ); } /** * For SIF_Request messages, gets the SIF versions responses should conform to.<p> * @return The value of the <code>SIF_Request/SIF_Version</code> element or * null if the message is not a SIF_Request message */ public SIFVersion[] getSIFRequestVersions() { return (SIFVersion[]) fObjects.get("SIF_RequestVersions"); } /** * For SIF_Request messages, gets the latest SIF version * that was requested by the requestor and is supported by the ADK * @return The value of the <code>SIF_Request/SIF_Version</code> element or * null if the message is not a SIF_Request message */ public SIFVersion getLatestSIFRequestVersion() { return ADK.getLatestSupportedVersion( getSIFRequestVersions() ); } /** * For SIF_Responze messages, returns information about the original SIF_Request, * including any custom user data that was assigned to the * {@link Query#setUserData(Serializable)} member.<p> * @param requestInfo a RequestInfo object representing the SIF_Request * @see #getSIFRequestInfo() */ public void setSIFRequestInfo( RequestInfo requestInfo ) { fObjects.put("SIFRequestInfo", requestInfo ); } /** * For SIF_Responze messages, returns information about the original SIF_Request, * including any custom user data that was assigned to the * {@link Query#setUserData(Serializable)} member.<p> * @return a RequestInfo object representing the request. * @see #setSIFRequestInfo(RequestInfo) */ public RequestInfo getSIFRequestInfo() { return (RequestInfo)fObjects.get( "SIFRequestInfo" ); } /** * For SIF_Request messages, identifies the type of object requested<p> * @param objType An ElementDef constant from the SIFDTD class */ public void setSIFRequestObjectType( ElementDef objType ) { fObjects.put( "SIF_RequestObjectType", objType ); } /** * For SIF_Request messages, identifies the type of object requested<p> * @return An ElementDef constant from the SIFDTD class */ public ElementDef getSIFRequestObjectType() { return (ElementDef)fObjects.get( "SIF_RequestObjectType" ); } /** * For SIF_Response messages, gets the packet number<p> * @return The Integer value of the <code>SIF_Response/SIF_PacketNumber</code> * element or null if the message is not a SIF_Response message */ public Integer getPacketNumber() { return (Integer)fObjects.get("SIF_PacketNumber"); } /** * For SIF_Response messages, determines if more packets are to be expected<p> * @return The string value of the <code>SIF_Response/SIF_MorePackets</code> * element or null if the message is not a SIF_Response message or the * element is missing. */ public Boolean getMorePackets() { String s = (String)fAttr.get("SIF_MorePackets"); return s == null ? null : new Boolean( s.equalsIgnoreCase("yes") ); } /** * For SIF_Request messages, gets the maximum packet size of result packets * @return The value of the <code>SIF_Request/SIF_MaxBufferSize</code> * element or zero if the message is not a SIF_Request message or the * buffer size could not be converted to an integer */ public int getMaxBufferSize() { Integer s = (Integer)fObjects.get("SIF_MaxBufferSize"); try { return s.intValue(); } catch( Throwable thr ) { return 0; } } public String getAttribute( String attr ) { return (String)fAttr.get(attr); } public void setAttribute( String attr, String value ) { fAttr.put(attr,value); } public String[] getAttributeNames() { return null; } /** * Preparses a raw SIF_Message to extract its header information and payload * element type. Applications that do not wish to fully parse a message but * need to know its header information can call this method and defer full * parsing to a later stage. When <code>keepContent</code> is true, the * message content is preserved as-is in the returned SIFMessageInfo object * and can be retrieved by calling getMessage on that object. * <p> * * For SIF_Ack messages, the value of <code><SIF_OriginalMsgId></code> * and <code><SIF_OriginalSourceId></code> are parsed and stored in * the SIFMessageInfo object. These are the only non-header elements that * are parsed. * <p> * * @param is The source of the SIF_Message to preparse * @param keepMessage If true, the string representation of the message is kept in memory * @param zone The zone from which this message was received * @return A SIFMessageInfo object containing information about the message * @throws IOException * @throws ADKMessagingException */ public static SIFMessageInfo parse( InputSource is, boolean keepMessage, Zone zone ) throws IOException, ADKMessagingException { BufferedReader in = null; try { String file = is.getSystemId(); if( file == null ){ throw new ADKMessagingException("InputSource has no data to parse",zone); } in = new BufferedReader( new FileReader(file) ); return parse( in, keepMessage, zone ); } finally { if( in != null ) in.close(); } } /** * Preparses a raw SIF_Message to extract its header information and payload * element type. Applications that do not wish to fully parse a message but * need to know its header information can call this method and defer full * parsing to a later stage. When <code>keepContent</code> is true, the * message content is preserved as-is in the returned SIFMessageInfo object * and can be retrieved by calling getMessage on that object. * <p> * * For SIF_Ack messages, the value of <code><SIF_OriginalMsgId></code> * and <code><SIF_OriginalSourceId></code> are parsed and stored in * the SIFMessageInfo object. These are the only non-header elements that * are parsed. * <p> * * @param in The source of the SIF_Message to preparse. The caller is responsible for closing the Reader * @param keepMessage If true, the string representation of the message is kept in memory * @param zone The zone from which this message was received * @return A SIFMessageInfo object containing information about the message * @throws IOException * @throws ADKMessagingException */ public static SIFMessageInfo parse( Reader in, boolean keepMessage, Zone zone ) throws IOException, ADKMessagingException { StringWriter out = null; try { if( keepMessage ) out = new StringWriter(); // Header info, payload type, and full message if desired SIFMessageInfo inf = new SIFMessageInfo(); inf.fZone = zone; int ch = -1; int elements = 0; int bytes = 0; boolean inTag = false; boolean inHeader = false; boolean storValue = false; boolean ack = false, response = false; // Buffer for retaining message content char[] buf = keepMessage ? new char[1024] : null; // Tag buffer size is 16*2, enough for largest SIF10r1 element name StringBuffer tag = new StringBuffer(16*2); // Value buffer size is 16*3, enough for GUID StringBuffer value = new StringBuffer(16*3); // SIF 1.0r1 Optimization: as soon as we parse the SIF_Header and // optionally the SIF_OriginalMsgId and SIF_OriginalSourceId elements // of a SIF_Ack we're done. So continue for as long as // required_elements != 3. // int required_elements = 0; while( in.ready() && required_elements != 3 ) { ch = in.read(); if( keepMessage ) { buf[bytes++] = (char)ch; if( bytes == buf.length-1 ) { out.write(buf,0,bytes); bytes=0; } } if( ch == '<' ) { inTag = true; storValue = false; } else if( ch == ' ' && inTag ) { inTag = false; storValue = true; // attributes follow } else if( ch == '>' ) { if( storValue ) System.out.println("Attributes: "+value.toString()); // We now have text of next element switch( elements ) { case 0: // Ensure first element is <SIF_Message> if( !tag.toString().equals("SIF_Message") ){ throw new ADKMessagingException("Message does not begin with SIF_Message",zone); } break; case 1: // // Payload element (e.g. "SIF_Ack", "SIF_Register", etc.) // Ask the DTD object for a type code for this message, store // it as the payload type in SIFMessageInfo. If zero, it means // the element is not recognized as a valid payload type for // this version of SIF. // inf.fPayload = ADK.DTD().getElementType(tag.toString()); if( inf.fPayload == 0 ){ throw new ADKMessagingException("<"+tag.toString()+"> is not a valid payload message",zone); } // Is this a SIF_Ack or SIF_Response? ack = ( inf.fPayload == SIFDTD.MSGTYP_ACK ); response = ack ? false : ( inf.fPayload == SIFDTD.MSGTYP_RESPONSE ); if( !ack && !response ) required_elements += 2; else if( response ) required_elements += 1; break; default: String s = tag.toString(); if( inHeader ) { // End of a header element, or </SIF_Header>... if( s.charAt(0) == '/' ) { if( s.equals("/SIF_Header") ) { inHeader = false; required_elements++; } else if( !( s.startsWith("/SIF_Sec") ) ) { inf.setAttribute(s.substring(1),value.toString()); } } else storValue=true; } else if( !inHeader ) { if( s.equals("SIF_Header") ) { // Begin <SIF_Header> // TODO: This class maintains SIF_Header information in the fHeader // variable. This particular parsing mechanism doesn't re-create a SIF_Header // element. Therefore if the parse method is used, the only way to get these // properties back out is to use the getAttribute() call. inHeader = true; } else if( ack ) { // SIF_Ack / SIF_OriginalSourceId or SIF_OriginalMsgId if( s.startsWith("SIF_Orig") ) storValue = true; else if( s.startsWith("/SIF_Orig") ) { required_elements++; inf.setAttribute(s.substring(1),value.toString()); } } else if( response ) { // SIF_Response / SIF_RequestMsgId if( s.startsWith("SIF_Req") ) storValue = true; else if( s.startsWith("/SIF_Req") ) { required_elements++; inf.setAttribute(s.substring(1),value.toString()); } } } value.setLength(0); break; } inTag=false; tag.setLength(0); elements++; } else { if( inTag ) tag.append((char)ch); else if( storValue ) value.append((char)ch); } } if( out != null ) { // Read the remainder of the input stream and copy it to the // output buffer if( bytes > 0 ) out.write(buf,0,bytes); while( in.ready() ) { bytes = in.read(buf,0,buf.length-1); out.write(buf,0,bytes); } // Store message content out.flush(); inf.fMessage = out.getBuffer().toString(); } return inf; } finally { if( out != null ) { out.flush(); out.close(); } } } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return fAttr.toString(); } /** * Gets the SIF Contexts that this message applies to * @return the SIF Contexts that this message applies to */ public SIFContext[] getSIFContexts() { return fContexts; } /* * TODO: * These added by Eric P on 3/2/09 in the initial 2.3.0 branch so that * ETF will continue to work. The use of these methods needs to be evaluated; * for example, some of these values should probably be assigned in the * constructor like they are for other SIF message types, but because the * SIF_ServiceXxx messages are not yet part of the ADK, that's not possible * at the moment. So, functions like setSIFServiceMsgId are being called * from elsewhere in the ADK code (search for that method to find where)... */ public String getSIFServiceMsgId() { return getAttribute("SIF_ServiceMsgId"); } public void setSIFServiceMsgId( String msgId ) { setAttribute( "SIF_ServiceMsgId", msgId ); } public String getSIFServiceName() { return getAttribute("SIF_ServiceName"); } public String getSIFServiceMethod() { return getAttribute("SIF_ServiceMethod"); } public HashMap<String, Object> getObjects() { return fObjects; } }