Sunday, March 30, 2008

Java: A JDialog and JProgressBar Example

If you have an application that runs for a long time, providing feedback that something is still happening is a really useful thing. Java provides a very wide range of GUI components.

One that fits the need is the JProgressBar. As you might expect, the JProgressBar allows you to draw a progress meter, either horizontally or vertically, and update it while your program is running.

Here is an example of using JProgressBar within a JDialog. JDialog allows you to set the dialog as modal, which will stop the application until the dialog is dismissed (as a JOptionPane does). There are many ways to create a dialog in java, this example includes both and is merely one (very minimalistic) way to do it.

First the JProgressBar object is created and attributes set, including min and max values of 0 and 100, respectively, and size of the progress bar:

JProgressBar pb = new JProgressBar(0,100);
pb.setPreferredSize(new Dimension(175,20));
pb.setString("Working");
pb.setStringPainted(true);
pb.setValue(0);


A JLabel object is created to display some text in the dialog:

JLabel label = new JLabel("Progress: ");


Then the label and progress bar are added to the panel:

JPanel center_panel = new JPanel();
center_panel.add(label);
center_panel.add(pb);



Once the panel is populated, the JDialog object is created, the panel is added,
and it is made visible:

dialog = new JDialog((JFrame)null, "Working ...");
dialog.getContentPane().add(center_panel, BorderLayout.CENTER);
dialog.pack();
dialog.setVisible(true);



Some useful actions to perform on the dialog, though not all at the same time:

dialog.setLocationRelativeTo(null); // center on screen
dialog.setLocation(550,25); // position by coordinates
dialog.toFront(); // raise above other java windows



While your program is running, you can update the progress bar by feeding it an integer value between the min and max values:

pb.setValue(25);


You have a couple of options when your program is done. The simplest thing to do is to close the dialog, and then display a JOptionPane, for example, informing the user:

dialog.dispose();


To get more creative, one can add a button to the dialog, which will can close the dialog when pressed. The action is defined by adding an action listener to the button. Here we dynamically add the button, but it could be done at the beginning.

JButton button = new JButton("Done");
button.setActionCommand("done");
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent evt){
if ( evt.getActionCommand().equals("done") ) {
dialog.dispose();
}
}
});


The button is added to a new JPanel object:

JPanel page_end_panel = new JPanel();
page_end_panel.add(button);


Then the new panel is added to the dialog, which will make it appear immediately:

dialog.getContentPane().add(page_end_panel, BorderLayout.PAGE_END);


It's not required, but if you wish to force the user to hit the button, the dialog can be made "modal", which will cause it to act like a JOptionPane. It seems that the visibility of the dialog must be toggled for the change in modality to take effect:

dialog.setModal(true);
dialog.pack();
dialog.setVisible(false);
dialog.setVisible(true);



This will cause the program to wait until the button is pressed, which will dismiss the dialog.

Just in case the dialog is dismissed by closing the window, without hitting the button, you might want to add a call to dispose() anyway. Perhaps that is unnecessary.
dialog.dispose();

Intralink Scripting: Triggering on Java Events

It is possible to trigger on events occuring with the JVM using the AWTEventListener class. There won't be fine grain control nor information about what event occurred, but that can be derived from the expected events.

This is an example of how to use the AWTEventListener to monitor for window events such as opening windows and closing windows, as well as windows gaining and losing focus.

The first step is to define an AWT event listener. This listener makes the newly created frame invisible. It's almost immediate, but the frame will appear briefly before disappearing.

AWTEventListener listener = new AWTEventListener() {
public void eventDispatched(java.awt.AWTEvent event) {
if (event.getID() == WindowEvent.WINDOW_OPENED) {
Frame[] frames = Frame.getFrames();
int frm_idx = frames.length-1;
System.out.println( " making frame " + frm_idx
+ " (" + frames[frm_idx].getTitle()
+ ") invisible" );
frames[frm_idx].setVisible(false);
}
}
};

Once the listener object has been created, it needs to be registered to become active using addAWTEventListener():

Toolkit.getDefaultToolkit().addAWTEventListener(listener, AWTEvent.WINDOW_EVENT_MASK);


To disable the listener, use removeAWTEventListener():

Toolkit.getDefaultToolkit().removeAWTEventListener(listener);


Monday, March 24, 2008

Intralink: Who's Logged In?

A PTC/User post of mine from May 2002:

Use this sql query as a privileged user to list intralink users who have logged into the dataserver:

  col un format A15 heading 'User Name'
col mn format A15 heading 'Host Name'
col lt format A10 heading 'Login Date'
col lc format 999999999 heading 'Last Cmd (Sec)'
select
OSUSER un,MACHINE mn,STATUS,TERMINAL,LOGON_TIME lt,LAST_CALL_ET lc
from v$session where SCHEMANAME='PDM';
The 'Status' column will show 'ACTIVE' or 'INACTIVE', with 'ACTIVE' indicating that there is ongoing sql activity (i.e. Search, Checkout, Checkin, etc.). The 'Last Cmd...' column shows the number of seconds since the last sql command issued by the client. It's only updated every three seconds.

An entry will appear immediately when the user logs in, and will disappear immediately when the user logs out. The entry will remain even if the client license on your FlexLM license server has timed out.

Extracting Class Info with Java Reflection

Here is some code that I use to extract information about classes using Java Reflection. It takes the class name as a String argument (i.e. "java.util.Date"), then outputs the information to standard output.

It works best when incorporated into a loop reading class names from an input file or some other data source. For you Intralink Scripting users out there, the data will appear in your .proi.log file.


public void getClassData ( String className ) throws Exception {

System.out.println( "Class: " + className );
System.out.println();

try {

Class c = Class.forName(className);

System.out.println( " package: " + c.getPackage().getName() );
System.out.println( " isinterface: " + c.isInterface() );

System.out.print( " superclass: " );
if (c.getSuperclass() == null) { System.out.println(); }
Class sc = c;
int indent = 0;
while (sc.getSuperclass() != null) {
if (indent > 0) {
for (int i=0; i<indent; i++) { System.out.print( " " ); }
System.out.print( " -> " );
}
System.out.println( sc.getSuperclass().getName() );
sc = sc.getSuperclass();
indent += 3;
}

System.out.println();


System.out.println("Constructors: ");
try {
// Constructor[] cons = c.getDeclaredConstructors();
Constructor[] cons = c.getConstructors();
for (int i = 0; i < cons.length; i++) {
// System.out.println( " constr: " + cons[i].toString() );
System.out.println( " " + cons[i].toString() );
}
}
catch (Exception e1) {
// cannot get constructor info
}
System.out.println();


System.out.println("Interfaces: ");
try {
Class intfs[] = c.getInterfaces();
for (int i = 0; i < intfs.length; i++) {
// System.out.println( " intf: " + intfs[i].toString() );
System.out.println( " " + intfs[i].toString() );
}
}
catch (Exception e1) {
// cannot get interface info
}
System.out.println();


System.out.println("Methods: ");
try {
// Method meths[] = c.getDeclaredMethods();
Method meths[] = c.getMethods();
for (int i = 0; i < meths.length; i++) {
// System.out.println( " method " + meths[i].getName() + "(): " + meths[i].toString() );
System.out.println( " " + meths[i].getName() + "(): " + meths[i].toString() );
}
}
catch (Exception e1) {
// cannot get method info
}
System.out.println();


System.out.println("Fields: ");
try {
// Field flds[] = c.getDeclaredFields();
Field flds[] = c.getFields();
for (int i = 0; i < flds.length; i++) {
// System.out.println( " field " + flds[i].getName() + ": " + flds[i].toString() );
System.out.println( " " + flds[i].getName() + ": " + flds[i].toString() );
}
}
catch (Exception e1) {
// cannot get method info
}
System.out.println();


}
catch (Exception e) {
System.out.println( " Problem getting class information!" );
System.out.println( e.getMessage() );
System.out.println();
}


System.out.println();
System.out.flush();

}

This is easily modified to output data to a named file. Let me know if you need to see that.

Friday, March 21, 2008

Intralink Commonspace Search for Locked Objects

A PTC/User post of mine from Sep 2002:

The official way:

Check out http://www.ptc.com/cs/tan/106363.htm for a TAN describing how to add a search for Commonspace Status (aka Status Description). Also check out http://www.ptc.com/cs/tan/106893.htm for another TAN describing how to add additional filtering options using a similar workaround.

The trick for Locate is to define and save a search for Release Level, then edit your ilprefs.txt file to change the search to Status Description. In the ilprefs.txt file the 'Operand' for a Release Level search is 7. This needs to be changed to 14 for a Status Description search. Set the 'Value' field to be 'Lock*' to search for all locked objects. Set it to '*username*' for objects locked by or shared with a user and also those with Intent to Modify.

After figuring out how to get it to work consistently, I'm beginning to see why searches for Status Description were 'omitted'. I think the developers found that this type of search didn't work right and excluded it from the gui. It seems as if Locate must find everything first, and then it can search for a specific value.

Here is a method that works every time, unfortunately it also takes a really, really long time:

  1. Make sure 'Status Description' is a displayed column in the results section.
  2. Search for: Status Description='*' and Latest Revision/Version. Bypassing this step seems always to produce no results. This isn't needed for every search, but it is needed at least once per session.
  3. Search for: Status Description='*something*'. Where 'something' is what you were looking for in the first place.

The unofficial way:

If you want much faster results (1-2 seconds), you can use the following SQL query to show locked objects and the user who owns the lock. Replace 'joe' with the user name, or use the '%' wildcard (slower). The 'set' and 'col' statements are optional, but help clean up the output.

set linesize 200
col username format A15 heading 'User Name'
col PINAME format A35 heading 'Object Name'
col MODIFIEDON format A15 heading 'Date Changed'
col FOLPATH format A75 heading 'Folder Name'

select
usr.username,
pi.piname,
brlock.MODIFIEDON,
fld.folpath
from
pdm.PDM_BRLOCK brlock,
pdm.PDM_BRANCH branch,
pdm.PDM_PRODUCTITEM pi,
pdm.PDM_USER usr,
pdm.PDM_FOLDER fld
where
brlock.brid=branch.brid
and branch.piid=pi.piid
and brlock.userid=usr.userid
and pi.folid=fld.folid
and usr.username like 'joe'
;

To see users with whom a lock is shared and the corresponding objects, use this SQL query. Replace 'joe' with the user name, or use the '%' wildcard.

set linesize 200
col username format A15 heading 'User Name'
col PINAME format A35 heading 'Object Name'
col FOLPATH format A75 heading 'Folder Name'

select
usr.username,
pi.piname,
fld.folpath
from
pdm.PDM_BRLOCK brlock,
pdm.PDM_BRLOCKSHARED brlockshared,
pdm.PDM_BRANCH branch,
pdm.PDM_PRODUCTITEM pi,
pdm.PDM_USER usr,
pdm.PDM_FOLDER fld
where
brlock.brid=branch.brid
and brlockshared.brlid=brlock.brlid
and branch.piid=pi.piid
and brlockshared.userid=usr.userid
and pi.folid=fld.folid
and usr.username like 'joe'
;


If you use this query and you get results that have old dates for TXNMSTARTTIME and/or TXNMFINISHTIME, then chances are you have objects that are Locked by System or some other similar problem.

set linesize 200
col username format A15 heading 'User Name'
col PINAME format A35 heading 'Object Name'
col FOLPATH format A75 heading 'Folder Name'

select
usr.username,
pi.piname,
txnmaster.TXNMSTARTTIME,
txnmaster.TXNMFINISHTIME,
fld.folpath
from
pdm.PDM_TXNMASTER txnmaster,
pdm.PDM_BRLOCK brlock,
pdm.PDM_BRANCH branch,
pdm.PDM_PRODUCTITEM pi,
pdm.PDM_USER usr,
pdm.PDM_FOLDER fld
where
txnmaster.txnmid=brlock.txnmid
and brlock.brid=branch.brid
and branch.piid=pi.piid
and txnmaster.userid=usr.userid
and pi.folid=fld.folid
;

Optimizing WebLink: A Better pfcCreate()

While performance testing a processing intensive WebLink application in Pro/Engineer, I came across something surprising. Use of the PTC supplied pfcCreate() function inside a loop made the application significantly slower.

The bigger the loop, the slower the application. For a small loop, the user probably wouldn't notice, but it is very noticeable when recursively processing 10,000+ part assemblies.

Here's what I was using that could be very slow:

for (int i=0; i<array.Count; i++) {
obj = pfcCreate("pfcBlahBlahBlah");
// do something here with 'obj'
}






I came to the conclusion that getting these objects from the COM server (on windows) was an expensive operation. Minimizing its use seemed to be a very good idea. My first though was to simply pull pfcCreate() out of the loop, which made it much faster. Problem solved!

Faster code:

obj = pfcCreate("pfcBlahBlahBlah");

for (int i=0; i<array.Count; i++) {
// do something here with 'obj'
}







Well, not quite.

The problem is that that chunk of code is used in some other loop, which could be used in some other loop, possibly in some other function, and so on. It was just not enough to rewrite the code in that way. Something else had to be done to eliminate that performance hit.

When it occurred to me that pfcCreate() always returned class objects - the same every time - then I had my answer. Rewrite pfcCreate() and enable caching of the returned objects.

Now, instead of hitting the COM server for every request, the function first performs a lookup of the class object in a global array (creating it if necessary). If the class object has previously been requested, it will be found in the global array, and that object will be returned. If it is not found, the class object is obtained from the COM server, stored in the global array, and then returned to the caller.

In addition to being more efficient when getting WebLink class objects, I now can write my code any way I want putting pfcCreate() inside or outside of the loop, no performance hit either way.


Here is the modified pfcCreate() that I use in my applications:


function pfcCreate (className) {
if (!pfcIsWindows())
netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");

if (className.match(/^M?pfc/)) {

// Check global object cache first, then return that object
//
try {
if (className in global_class_cache) {
return global_class_cache[className];
}
}
catch (e) {
// Probably no global_class_cache yet
global_class_cache = new Object();
}

}

// Not in global object cache, create object
var obj = null;

if (pfcIsWindows()) {
obj = new ActiveXObject("pfc."+className);
}
else {
obj = Components.classes ["@ptc.com/pfc/" + className + ";1"].createInstance();
}

// Return created object
//
if (className.match(/^M?pfc/)) {
global_class_cache[className] = obj;
}

return obj;
}

Wednesday, March 19, 2008

Printing from Intralink: Where'd it go?!

One difficulty new Intralink users have is finding data that is printed from the Intralink client. The 'html' folder hidden deeply in the .proi is not the first place users expect the output to go.

To counteract this annoyance, several of my users use this one-liner Intralink Scripting application to direct output to their working directory.

IL.setReportAdapterDefaultPath( System.getProperty("user.dir") );

The users run this once per session.

Orphaned Instances in Intralink Commonspace

A PTC/User post of mine from April 2003:

After noticing a few family table generics where instances were removed or omitted from the table in Pro/Engineer, I wrote the following sql query to find others in the Intralink Commonspace.

The logic works like this: look up the latest version of an instance name on the main branch, figure out the corresponding generic, and output if the associated generic is not the most recent generic.

If the most recent instance revision does not match up with the most recent generic revision, then the instance has been 'orphaned'. The output includes the revision and version of the instance and its associated generic.


set linesize 120
column INSTANCE format a35
column GENERIC format a35
column PIVREV format a6
column PIVVER format 99999

select
pii.PINAME as INSTANCE,
pivi.PIVREV,
pivi.PIVVER,
pig.PINAME as GENERIC,
pivg.PIVREV,
pivg.PIVVER
from
pdm.PDM_PRODUCTITEM pii,
pdm.PDM_BRANCH bri,
pdm.PDM_PRODUCTITEMVERSION pivi,
pdm.PDM_GENINSREL gis,
pdm.PDM_PRODUCTITEMVERSION pivg,
pdm.PDM_BRANCH brg,
pdm.PDM_PRODUCTITEM pig
where
bri.BRPATH='main' and
pii.PIID=bri.PIID and
bri.BRID=pivi.BRID and
bri.BRLASTVERID=pivi.PIVID and
pivi.PIVID=gis.INSTPIVID and
gis.GENPIVID=pivg.PIVID and
pivg.BRID=brg.BRID and
brg.PIID=pig.PIID and
brg.BRLASTVERID!=pivg.PIVID
order by GENERIC, INSTANCE
;

Tuesday, March 18, 2008

Using ObjectInfo and its Subclasses in Intralink Scripting

Data can be extracted from Intralink with Intralink Scripting in a number of ways. The getTableCellValue() method can be used to pull data from a displayed table or form, but sometimes this is just not enough.

For example, let's say you want to know, for certain Commonspace objects, the file name in the file vault. This can be accomplished in several ways. You can use Oracle SQL queries, but this requires administrative access to Oracle on the server, and we need something simpler.

Another option is to check out the files to a Workspace, then examine the contents of the 'File Path' column. This is easier than SQL, but can be a time consuming task, especially if data is needed on many objects.

It turns out that there's a simple and direct way to access the information without SQL and without checkouts. The Java code that provides the core of the Intralink client provides powerful classes and methods that can be used to tap directly into Intralink. A little bit of Java Reflection tells us all about it, including constructors, methods, and available fields.

One hierachy of classes to do this is the ObjectInfo class and subclasses. These provide very extensive information about objects, in Commonspace and Workspaces, without needing to access displayed fields in the Intralink GUI.

Here is a list of most 'ObjectInfo' derived classes:

com.ptc.intralink.ila.AdmRelLevelObjectInfo
com.ptc.intralink.ila.AdmRelSchemeObjectInfo
com.ptc.intralink.ila.AdmRoleObjectInfo
com.ptc.intralink.ila.AdmUserGroupObjectInfo
com.ptc.intralink.ila.AdmUserObjectInfo
com.ptc.intralink.ila.ApprovalSchemeObjectInfo
com.ptc.intralink.ila.AttrClassObjectInfo
com.ptc.intralink.ila.AttrDefObjectInfo
com.ptc.intralink.ila.CSBaselineObjectInfo
com.ptc.intralink.ila.CSFolderObjectInfo
com.ptc.intralink.ila.CSObjectInfo
com.ptc.intralink.ila.CSPIObjectInfo
com.ptc.intralink.ila.CSPIVObjectInfo
com.ptc.intralink.ila.CSSubmFormObjectInfo
com.ptc.intralink.ila.LILObjectInfo
com.ptc.intralink.ila.LOObjectInfo
com.ptc.intralink.ila.LOVObjectInfo
com.ptc.intralink.ila.ObjectInfo
com.ptc.intralink.ila.PIVDependencyObjectInfo
com.ptc.intralink.ila.RTPObjectInfo
com.ptc.intralink.ila.WSDependencyObjectInfo
com.ptc.intralink.ila.WSObjectInfo
com.ptc.intralink.ila.WSPIObjectInfo

Here is some code that utilizes parts of ObjectInfo and CSPIVObjectInfo to get the file path for selected Commonspace objects:

IL.selectAll( "PIV" );
String[] o = IL.getSelectedObjects( "PIV" );
IL.deselectAll( "PIV" );

for (int i=0; i<o.length; i++) {
CSPIVObjectInfo oi = (CSPIVObjectInfo)ObjectInfo.createByKey( ObjectInfo.tPIV, o[i] );
String filePath = oi.getFilePath();
System.out.println( o[i] + "\t" + filePath );
}

The tricky part was figuring out how to instantiate the ObjectInfo objects, but with a little trial and error, and the help of Java Reflection output, it doesn't take to long to figure it out.

Conditional Loading of Pro/Toolkit Applications

Pro/Toolkit applications often provide great value. However, not every user needs to be running all applications all of the time. Pro/Toolkit provides a few options for the startup of applications, but these options apply to all users. There aren't any options there to give you direct control on a per user basis. This can be accomplished, however, just in a different way, via the config.pro file.

The trick is to use an environment variable for the value of the PROTKDAT option. The syntax (for both unix and windows) would look like this:

PROTKDAT $SOME_TK_APP

For users that need the application, set the environment variable before Pro/Engineer starts to contain the path and file name of the Pro/Toolkit application registry file.

Here are some examples:

Unix C Shell:
setenv SOME_TK_APP /opt/myprogram/protk.dat

Unix Bourne/Bash Shell:
SOME_TK_APP=/opt/myprogram/protk.dat
export SOME_TK_APP

Windows Batch File:
set SOME_TK_APP=N:\apps\myprogram\protk.dat

In WF3 and earlier, when Pro/Engineer reads the config.pro, it will check the value of the environment variable. If the environment variable is not defined, or doesn't contain the path to a Pro/Toolkit registry file, the config.pro option is silently ignored. Otherwise, it reads in the registry file data and starts the application as instructed.

In WF4, things get a little different. If your environment variable is empty or points to a non-existent file, a dialog box will appear that mentions this "problem". Since the only option in the dialog box is an OK button, the dialog box seems rather pointless and and makes for an annoying user experience.

Anyway, to workaround this "bug", simply have the environment variable point to an empty file when the application is not needed. As a result of this, in my Pro/Toolkit app installs, I typically have two Pro/Toolkit registry files. One respresents the "on" state, while the other (the empty file) respresents the "off" state.

As an alternative to an empty file, the "off" state registry file could have the DELAY_START option set to TRUE. This would still allow for the program to be used, but would not start it up when Pro/Engineer starts. This is a little more flexible because otherwise the application is completely unavailable to the user.


Be sure to read my followup article, Pro/Toolkit: Simplifying Your protk.dat File with Environment Variables.