Monday, September 09, 2013

Writing to stdout and stderr Take II

A few years ago I wrote a post that describes how to print text to the App Engine output file, bypassing the verbose MessageBox statement with its text length limitation (AppEngine Output Tricks, Reporting, Logging, Etc). I recently employed that same technique for logging Taleo Connect Client output when run from an App Engine. Looking at the code from those old println methods, I noticed a couple of inefficiencies:

  • Each invocation of println initializes the same JavaObject variables resulting in wasted CPU cycles and wasted memory
  • The two functions, println_to_stderr and println_to_stdout, are nearly identical which violates the DRY principle

The solution to both of these problems is inherent in Application Classes. We can solve the first issue by maintaining state inside the App Class with private instance variables. The DRY violation could be solved through properly implemented composition (preferred) or inheritance.

Here is an alternate implementation of the println_to_stdout method that uses an App Class to maintain state between invocations:

class StdoutWriter
   method println(&message As string);
   
private
   
   instance JavaObject &printlnMethod_;
   instance JavaObject &outputStream_;
end-class;

method println
   /+ &message as String +/
   /+ Extends/implements NAA_STDIO:IOWriter.println +/
   
   REM ** Lazy initializtion to ensure initialized between invocations;
   REM ** Need a local copy of member for "None" test;
   Local JavaObject &outputStream = &outputStream_;
   If (None(&outputStream)) Then
      REM ** NOTE: this is the only difference between StderrWriter and StdoutWriter;
      &outputStream_ = GetJavaClass("java.lang.System").out;
      
      Local JavaObject &stringClass = GetJavaClass("java.lang.Class").forName("java.lang.String");
      Local JavaObject &printStreamCls = &outputStream_.getClass();
      Local JavaObject &printlnArgTypes = CreateJavaObject("java.lang.Class[]", &stringClass);
      
      &printlnMethod_ = &printStreamCls.getDeclaredMethod("println", &printlnArgTypes);
   End-If;
   
   &printlnMethod_.invoke(&outputStream_, CreateJavaObject("java.lang.Object[]", &message));
   rem ** I didn't find flushing necessary, but here is where you would flush the buffer if desired;
   rem &outputStream_.flush();
end-method;

Notice that I used lazy initialization and only persist two JavaObject variables. The life of a JavaObject variable is potentially shorter than most PeopleCode variables. The PeopleSoft runtime can persist many types of PeopleCode variables across requests (events, think time functions, etc). This is not the case with Java variables. Lazy initialization and re-initialization ensures those Java variables always have a value. I only persist two variables rather than the seven from the original println_to_stdout function because we only need two of those variables for subsequent invocations.

About the DRY principle violation... I chose not to solve it. The number of lines required to create another app class (for composition or inheritance) was about the same as the number of duplicate lines. If I had multiple targets besides stderr and stdout, then creating a class structure to contain this redundant code would make sense. In this case the clarity seemed worth a little redundancy.

So how do you use it? Assuming you put this class in an Application Package named JJM_STDIO, you would call it like this:

import JJM_STDIO:StdoutWriter;

Local JJM_STDIO:StdoutWriter &out = create JJM_STDIO:StdoutWriter();

&out.println("This could be a very long line of text read from some process output that would exceed the maximum length for MessageBox");

6 comments:

Family Medicine Austin said...

does this help in all modules of

Jim Marion said...

@Arthur, it works everywhere, but is best in App Engines.

Jim Marion said...

Special thanks to Brian Kim for pointing out that I had a mismatch between the class definition and the usage example (StdoutWriter and StderrWriter). I updated the sample above. Brian, I accidentally deleted your comment. I hope this serves as satisfactory attribution.

Tom Mannanchery said...

In a content provider (HCM, FSC) if a component like User Profiles is accessed and a record is loaded, navigating to another component with same keys loads the record automatically.
This doesn't happen when remote components are accessed via Interaction Hub. Have you noticed this behavior? Were you able to solve it? We have created an SR (SR 3-9802791581)with Oracle Support, nbt have been told this will be fixed only in Tools 8.55

Jim Marion said...

@Tom, I assume that you mean when transferring between components with a similar key structure but different pillar? For example, HCM and FSCM? That would make sense because they have different state blocks on different web servers.

Tom Mannanchery said...

Hey Jim,
Not between pillars - within the same pillar itself. Eg, if you are in HCM and have a user profile open, if you go to the Copy User Profile component, the user profile would already be loaded. But if you the same action via Unified Navigation through PIH, it doesn't work for us. Does it work for you?

Thanks,
Tom