Java Meets Novell's GroupWise API Gateway, Part II
Articles and Tips: article
Senior Consultant
Consulting Services
AL YOUNG
Senior Research Engineer
Developer Information
DAYLE S WOOLSTON
Senior Software Engineer
Advanced Development Group
01 Jul 1997
Looks at parsing a GroupWise header file and receiving data via GroupWise.
Introduction
As with the first installment in this article series, we assume a readership of application developers that may be new to object-oriented analysis and design, as well as being somewhat new to Java.1 As announced in the previous article, the discussion in the present installment focuses on parsing a GroupWise header file.2
In part 1 of this article series, we presented a "hypothetical publishing environment" as a context for which our sample Java classes might be developed. And in the first installment of our discussion we focused on "data to be distributed" via the E-mail part of such a publishing environment.
In this installment of our discussion, we focus on receiving data via GroupWise. Figure 1, for example, shows the same environment we introduced in the previous article; however, we now focus on the possibility that the same users who receive the "data to be distributed" may want to use GroupWise to submit data by which we can update our hypothetical database.
Figure 1: Expanded use of GroupWise within hypothetical publishing environment.
In the previous article we talked about the need for our Java classes to be able to write a GroupWise header file so that such a file could be distributed by GroupWise. In this article we shift our focus from writing a GroupWise header file to reading a GroupWise header file. Note that we limit our discussion to reading such files once received. We do not talk about how to receive them, nor do we discuss how to reconcile their contents with the contents of a database.
We want our Java application(s) to be able to read ASCII text files of the format required by the GroupWise API Gateway. Information on format requirements governing such files is available, as part of the GroupWise Gateway API Help, under the heading "Introduction to Keywords and API Files (Overview)." Nine topics appear under the foregoing heading. Of particular relevance to this discussion is the topic "Keyword Ordering Requirements and Delimiters."
Figure 2: Sample GroupWise file.
1: WPC-API= 1.2; 2: Header-Char= T50; 3: Msg-Type= MAIL; 4: From-Text= admin; 5: From= MERCURY1.NCS.admin; 6: To= Mercury.Mercury1(Updater); 7: All-To= Mercury.Mercury1(Updater); 8: Msg-Id= 321C92E2.125F.0001.000; 9: To-Text= Mercury.Mercury1.Updater; 10: Subject= Postman test 1; 11: Date-Sent= 22/8/96 11:03; 12: Security= Normal; 13: Send-Options= Notify-User; 14: Status-Request= Delivered, Opened; 15: Msg-Priority= Normal; 16: Attach-File= 17: Current-File=321c3e83; 18: Original-File=AUTOEXEC.BAT; 19: Size= 246; 20: Date= 27/7/96 11:47;, 21: Current-File=321c3e84; 22: Original-File=AUTOEXEC.OLD; 23: Size= 45; 24: Date= 26/10/95 4:13;, 25: Current-File=321c3e85; 26: Original-File=HELLOTST.NLM; 27: Size= 780; 28: Date= 16/8/96 4:37; 29: ; 30: -END-
Figure 2 presents part of the text from a sample file of the kind we want our Java application(s) to read. On line 1 of Figure 2, WPC-API appears as the keyword for a record in the file. Information available under "Keyword Ordering Requirements and Delimiters" in the help documentation states that "a header file must begin with the keyword WPC-API. WPC-API must be the first seven characters or the gateway will not process the file."
Each line in Figure 2 constitutes a record in a GroupWise header file. WPC-API is the keyword for the first record in the file. Each record in the sample appearing in Figure 2 contains a keyword and variable data belonging to the record identified by the keyword. The variable data in a record is distinguished from the record's keyword by an equal sign. A semicolon signals the end of variable data within a record, and either a line feed or a carriage-return and line feed terminates a record.
In the previous article in this series, we presented an object model in which each record in a GroupWise header file is represented by a class. Each instance of such classes inherits or implements write() behavior that enables an instance to create a textual representation of itself and output that representation onto an instance of Java's PrintStream class.
For example, the record appearing in Figure 2 on line 10 contains the keyword and variable data belonging to the subject field of a GroupWise message. In the object model presented in the previous installment in this article series, the Subject class is implemented as the class of objects serving as corollaries to the kind of record appearing at line 10. In that article, we talked about how an instance of the Subject class might go about creating the kind of record appearing on line 10. In this article, we talk about how a Java application might read the records in such a file.
Inasmuch as we want to update our relational database from GroupWise header files containing technical information received from interested parties, we must be able to extract the desired information from a header file. Accordingly, each of the following classes in our object model must be able to respond appropriately to a parse()request:
AllTo
AllToCc
ConversionAllowed
CurrentFile
DateField
DateSent
EndField
FileSize
From
FromText
HeaderCharacter
MessageFile
MessageId
MessagePriority
MessageType
OriginalFile
Security
SendOptions
StatusRequest
Subject
To
ToBc
ToCc
ToCcText
ToText
WpcApi
Referring to instances of the Subject class, for example, we want each instance to be able to parse an input stream (on a GroupWise header file) so that the instance will know which characters from an input stream should be assigned to the instance's valuevariable.
It may be remembered from the previous article in this series that write() methods implemented throughout the object model hierarchy accept as a parameter an instance of Java's PrintStream class. Each write method uses the instance of PrintStream as its output medium. Similarly, we implement a parse() behavior that expects an instance of an input stream; however, instead of extending the functionality of Java's stream classes, we create GwapiTokenInputStream as a subclass of Object, and assign instances of InputStreamand PushBackInputStream to instances of GwapiTokenInputStream.
Figure 3 illustrates the relationship among instances of these classes, in the style of a structure diagram showing association roles between the classes.3 The rolename for each association with a GwapiTokenInputStream is presented.
Figure 3: GwapiTokenInputStream and associated stream classes.
To parse GroupWise header files, we do not need to extend the functionality provided by Java's InputStream and PushBackInputStream classes; however, we do need to provide an encapsulated way of using Java's stream functionality. For example, we need to be able to get from a stream the keyword for any GroupWise header file's record. We might have created a subclass of a Java stream class and given it a getKeyword() method; however, doing so would have provided a very domain (or application) -specific behavior to what would otherwise be a general-purpose class.
Alternatively, we might have provided a getKeyword() method within the ContentComponentModel or Message hierarchies; however, such an implementation would have coupled parsing functionality (e.g., stream management) too tightly, in our opinion, to that hierarchy.
We have opted instead to encapsulate parsing behavior in a class that would know how to interact with streams, and yet which would not be (properly speaking) an extension of Java's stream classes. Accordingly, GwapiTokenInputStream encapsulates stream management functionality specific to our application domain.
Code Listing 1 presents the constructor by which GwapiTokenInputStream is instantiated.
Code Listing 1
1: public GwapiTokenInputStream(InputStream anInputStream, TokenNode[] keywords) { 2: pushedBack = false; 3: inputWithLineNumbers = new LineNumberInputStream(anInputStream); 4: input = new PushbackInputStream(inputWithLineNumbers); 5: buffer = new char[20]; 6: if (keywords == null) 7: keywords = new TokenArray(keywords); 8: }
Before we look more closely at this constructor, let's examine the series of messages that brings us to the point of needing to create an instance of GwapiTokenInputStream. The series of messages leading up to and including the construction of such an input stream is illustrated in Figure 4, which presents a sequence diagram4of such information.
Note that in Figure 4, relevant code listings are indicated. (Also, reference to Figure 6 in the first installment in this article series may provide additional context for understanding the parse()behavior among the sample Java classes.)
Figure 4: Sequence diagram of objects and messages involved in getting ready to parse a GroupWise header file.
The application in which our sample Java classes exist monitors the GroupWise Gateway directory structure where incoming GroupWise files are placed. Upon discovering an incoming file in the appropriate directory5, the Java application sends the following message to the Message class:
Message(aFile, bFile)
The foregoing message is a constructor that expects fileA to be a GroupWise header file, and fileB to be an attached file. Code Listing 2 presents the code constituting the Message(fileA, fileB) constructor. Note that as stated at the outset of this article, the Java application classes that monitor the GroupWise Gateway directory structure, and that ask the Message class to construct an instance of itself, are outside the scope of this article.
Code Listing 2
1: public Message(File aFile, File bFile) throws java.io.IOException { 2: Message aMessage = new Message(); 3: aMessage.parse(aFile, bFile); 4: }
The reason for the existence of such a constructor is to encapsulate the instantiation of a message within the Message class. Without such a constructor, the application in need of an instance would first have to ask the Message class for an instance, then the application class would have to ask that instance to parse the files involved. With the foregoing constructor, all these requests are subsumed in a single message.
Code Listing 3 presents the contents of the parse() method invoked in the code in Code Listing 2.
Code Listing 3
1: public void parse(File aFile, File bFile) throws java.io.IOException { 2: clear(); 3: file = aFile; 4: InputStream anInputStreamOnFile = new FileInputStream(aFile); 5: GwapiTokenInputStream anInputStream = new GwapiTokenInputStream(anInputStreamOnFile, ContentComponentModel.keywords); 6: parseMe(anInputStream, bFile); 7: }
In Code Listing 3, the clear() method simply assigns null to each variable belonging to the receiver (i.e., the instance of Message receiving the parse() request). Once the variables are reset to null, the GroupWise header file, passed in to the method as aFile, is assigned to the receiver's file variable. An instance of Java's FileInputStream, created on the GroupWise header file, is assigned to the local variable6 anInputStreamOnFile. On line 5, another temporary variable -- anInputStream -- is assigned an instance of GwapiTokenInputStream, created using the constructor method presented in Code Listing 1. Finally, parseMe()is sent to the receiver.
The sequence diagram appearing in Figure 4 summarizes the relationship among behaviors provided in code listings 1 - 3. Having, thus, presented the context in which an instance of GwapiTokenInputStream is requested, we now move to a more detailed examination of the construction of such an instance. Throughout this discussion we refer to the class diagram, for GwapiTokenInputStream, appearing in Figure 5.
Figure 5: GwapiTokenInputStream class diagram.
In the constructor appearing in Code Listing 1, the instance variable pushedBackis assigned false. The pushedBack variable is used in determining location during parsing, as will be seen later.
The inputWithLineNumbers variable is assigned an instance of LineNumberInputStream instantiated with the input stream created on the GroupWise header file in the method in Code Listing 3.
The receiver's input variable is assigned an instance of PushbackInputStream, created on the receiver's inputWithLineNumber variable contents (i.e., the instance of LineNumberInputStream created and assigned on line 3 in Code Listing 1).
The buffer variable is assigned an instance of Character, with a length of 20.
Finally, the keywords variable is instantiated only when it is found not to contain an array already. The test on line 6 of Code Listing 1 determines whether the keywords variable has anything in it. If it does, nothing is assigned to it. If the variable doesn't contain anything, an array of keywords is constructed and assigned.
We now turn to a discussion of line 6 in Code Listing 3, in which a Message must respond to the parseMe() message. Code Listing 4 presents a simplified version of a message's parseMe() method. The simplification involves the fact that only the subject field of a GroupWise header file is presented. Ellipses between lines 9 and 10 indicate where code in a more complete listing would appear.
Code Listing 4
1: protected void parseMe(GwapiTokenInputStream anInputStream, File bFile) { 2: boolean finishedParsing = false; 3: while (!finishedParsing) { 4: int aToken = anInputStream.getKeyword(); 5: if(aToken == ContentComponentModel.SUBJECT_TOK) { 6: anInputStream.pushBackKeyword(); 7: subject = new Subject(); 8: subject.parse(anInputStream); 9: anInputStream.getSemicolon(); . . . 10: } 11: } 12: }
The finishedParsing variable on line 2 in Code Listing 4 is a temporary variable, used in the while-test on line 3. While parsing remains incomplete, anInputStream is asked to getKeyword(), which returns the next keyword available for parsing in anInputStream. The if statement on line 5 tests the keyword returned from the behavior at line 4. If, in the case of our example, the keyword matches the contents of the SUBJECT_TOK class variable belonging to ContentComponentModel, lines 6 - 9 are executed.
On line 6 of Code Listing 4, the pushBackKeyword() message is sent to anInputStreamso that anInputStream will be ready to appropriately respond to the behavior called for on line 8. The pushBackKeyword() method belonging to anInputStream simply sets anInputStream's pushedBack value to true. (See Code Listing 5.) Having anInputStream do so means that when the subject object is asked to parse(anInputStream) the parsing includes the keyword for the subject field in the GroupWise header file.
Code Listing 5
1: pushBackKeyword() { 2: pushedBack = true; 3: }
Line 7 of Code Listing 4 assigns to the receiver's subject variable an instance of Subject.
Line 8 then asks the new instance of Subject to parse(anInputStream). The parse() behavior requested of a subject, is inherited from StringModel, Subject's immediate superclass. Code Listing 6 presents that behavior as it is implemented in StringModel.
Code Listing 6
1: void parse(GwapiTokenInputStream anInputStream) { 2: super.parse(anInputStream); 3: value = anInputStream.getValue(); 4: }
At line 2 in Code Listing 6, the parse() request is handed to StringModel's superclass, ContentComponentModel. In ContentComponentModel, the parse()behavior pertains only to retrieving from anInputStream the text of the keyword belonging to a record. Thus, by passing the parse() request to a superclass, the behavior for retrieving keywords from anInputStream is shared by all subclasses. Code Listing 7 shows the parse() behavior implemented in ContentComponentModel.
Code Listing 7
1: void parse(GwapiTokenInputStream anInputStream) { 2: int aToken = anInputStream.getKeyword(); 3: }
Upon completing the behavior specified in Code Listing 7, execution returns to line 3 of Code Listing 6, at which anInputStream is asked to return the value belonging to the record being parsed. Code Listing 8 presents the getValue() method implemented for each instance of GwapiTokenInputStream.
Code Listing 8
1: String getValue() { 2: if(pushedBack) 3: contents = null; 4: skipWhite(); 5: int aCurrentCharacter = input.read(); 6: int anIndex = 0; 7: do { 8: if(anIndex >= buffer.length) { 9: char newBuffer = new char[buffer.length * 2]; 10: System.arraycopy(buffer, 0, newBuffer, 0, buffer.length); 11: buffer = newBuffer 12: } 13: buffer[i++] = (char) aCurrentCharacter; 14: aCurrentCharacter = input.read(); 15: } 16: while ((aCurrentCharacter != ';') && (aCurrentCharacter != '\n') && (aCurrentCharacter != '\r')); 17: input.unread(aCurrentCharacter); 18: contents = String.copyValueOf(buffer, 0, anIndex); 19: return contents; 20: }
The if statement on line 2 (Code Listing 8) ensures that the variable data (exclusive of the key and the operator) associated with a record in a GroupWise header file is the only part of such a record that will be parsed for assignment to the value variable mentioned on line 3 of Code Listing 6.
In other words, if anInputStream (which is the receiver performing the behavior specified in Code Listing 8) is pushedBack, the receiver's contents variable is set to null, and the receiver skips any white space that may exist between the equal-sign operator and the beginning of the variable data in the record being parsed.
At line 5 (Code Listing 8), aCurrentCharacter, which is a temporary variable within the getValue() method, is assigned the result of asking the contents of the receiver's input variable (see line 4 of Code Listing 1) to perform its read() method. (Theread() method is part of the behavior provided for each instance of PushBackInputStream, and is not described in this article.)
At line 6, another temporary variable, named anIndex, is declared and zero is assigned as its initial content.
The do statement beginning at line 7 contains a test at line 8 to ensure that iteration across the contents of the receiver's buffer variable does not attempt to transcend the size of the buffer.
At line 9, another temporary variable named newBuffer is created as an instance of char whose size is twice that of the receiver's buffer.
After invoking the System's arraycopybehavior (at line 10), newBuffer is assigned to the receiver's buffervariable.
At line 13, the contents of the receiver's buffer, at anIndex++, is tested to see whether it is the same as the value currently assigned to aCurrentCharacter. If it is, aCurrentCharacter is reset to the result of input.read(), as shown at line 14.
Then, while aCurrentCharacter is either a semicolon, a new line, or a carriage return, the character is copied from the receiver's buffer and appended to the receiver's contents, which is returned at line 20 (Code Listing 8) so that the contents can be assigned to the value instance-variable at line 3 of Code Listing 6.
ContentComponentModel Hierarchy Parse() Methods
As we discussed in the previous article in this series, behaviors such as parse() may necessarily vary from one object to another. Parsing a record, in a GroupWise header file, that contains a date necessarily involves behavior different from parsing a record containing recipient names. The architecture described in the previous article enables the polymorphism of having objects respond to a parse() request while hiding the details of the manner in which each object actually behaves in response to such a request.
To accommodate the different ways in which the variable data in GroupWise header file records must be parsed, the design of our sample Java classes suggests that parse() methods be implemented in the following classes within the ContentComponentModel hierarchy:
AddressModel
AssignmentModel
AttachedFile
DateModel
EnumerationModel
FileModel
StringModel
Some Benefits
Enabling members of the ContentComponentModel hierarchy to respond to a parse() request, and implementing GwapiTokenInputStream as a means of insulating ContentComponentModel objects from stream management, provides the following benefits:
ContentComponentModel objects are insulated from changes that might arise in stream classes. Similarly, an application's stream management is insulated from changes in the nature of material to be parsed.
Behavioral peculiarities associated with parsing any particular record in a GroupWise header file are dissociated from the parsing of any other records in such a file. Application maintenance and enhancement relative to record format is limited to the object(s) concerned with affected record(s).
Additions, deletions, and reordering of records can be accomplished without significantly modifying either the ContentComponentModelhierarchy or GwapiTokenInputStream.
References
Johnson, Ashley; Young, Al; Woolston, Dayle S. "Java Meets Novell's GroupWise API Gateway, Part 1," Novell Developer Notes(vol. 4, no. 5), May 1997, p. 3.
See the header file topic in the GroupWise API online help. Four topics are available under the heading: Header File and Addressing Syntax, Keyword Ordering Requirements and Delimiters, Types of API Files, Understand a Typical Header File.
Unified Modeling Language Notation Guide version 1.0. Rational Software Corporation: Santa Clara, California, 13 January 1997, p. 38. Ibid., p. 66.
See the directory structure index entry in the list of help topics for the GroupWise API Gateway Help.
Flanagan, David. Java in a Nutshell: A Desktop Quick Reference for Java Programmers. O'Reilly & Associates, Inc.: Sebastopol, California, 1996, p. 20.
* Originally published in Novell AppNotes
Disclaimer
The origin of this information may be internal or external to Novell. While Novell makes all reasonable efforts to verify this information, Novell does not make explicit or implied claims to its validity.