Monday, November 28, 2022

Database Error Authenticating a WebService Call

My Saturday turned out differently than I had planned.  

A customer was a database error trying to authenticate a webservice call.  The first error message and stack trace was:


BMXAA6714E - The data for the next record in the mboset could not be retrieved for the SQL query select * from maxuser  where loginid = :1. See the log file for more details about the error.
java.sql.SQLException: ORA-01008: not all variables bound
	at oracle.jdbc.driver.T4CTTIoer11.processError(T4CTTIoer11.java:509) ~[oraclethin.jar:?]
	at oracle.jdbc.driver.T4CTTIoer11.processError(T4CTTIoer11.java:461) ~[oraclethin.jar:?]
	at oracle.jdbc.driver.T4C8Oall.processError(T4C8Oall.java:1104) ~[oraclethin.jar:?]
	at oracle.jdbc.driver.T4CTTIfun.receive(T4CTTIfun.java:550) ~[oraclethin.jar:?]
	at oracle.jdbc.driver.T4CTTIfun.doRPC(T4CTTIfun.java:268) ~[oraclethin.jar:?]
	at oracle.jdbc.driver.T4C8Oall.doOALL(T4C8Oall.java:655) ~[oraclethin.jar:?]
	at oracle.jdbc.driver.T4CStatement.doOall8(T4CStatement.java:229) ~[oraclethin.jar:?]
	at oracle.jdbc.driver.T4CStatement.doOall8(T4CStatement.java:41) ~[oraclethin.jar:?]
	at oracle.jdbc.driver.T4CStatement.executeForDescribe(T4CStatement.java:765) ~[oraclethin.jar:?]
	at oracle.jdbc.driver.OracleStatement.executeMaybeDescribe(OracleStatement.java:983) ~[oraclethin.jar:?]
	at oracle.jdbc.driver.OracleStatement.doExecuteWithTimeout(OracleStatement.java:1168) ~[oraclethin.jar:?]
	at oracle.jdbc.driver.OracleStatement.executeQuery(OracleStatement.java:1362) ~[oraclethin.jar:?]
	at oracle.jdbc.driver.OracleStatementWrapper.executeQuery(OracleStatementWrapper.java:369) ~[oraclethin.jar:?]
	at psdi.mbo.MboSet.getNextRecordData(MboSet.java:3350) [businessobjects.jar:?]
	at psdi.mbo.MboSet.fetchMbosActual(MboSet.java:2958) [businessobjects.jar:?]
	at psdi.mbo.MboSet.fetchMbos(MboSet.java:2915) [businessobjects.jar:?]
	at psdi.mbo.MboSet.getMbo(MboSet.java:2123) [businessobjects.jar:?]
	at psdi.mbo.MboSet.isEmpty(MboSet.java:4427) [businessobjects.jar:?]
	at psdi.security.SecurityService.commonUserValidation(SecurityService.java:1373) [businessobjects.jar:?]
	at psdi.security.SecurityService.authenticateUserM(SecurityService.java:938) [businessobjects.jar:?]
	at psdi.security.SecurityService.authenticateUser(SecurityService.java:466) [businessobjects.jar:?]
	at psdi.security.SecurityService.authenticateUser(SecurityService.java:348) [businessobjects.jar:?]
	at psdi.iface.util.SecurityUtil.getNewUserInfo(SecurityUtil.java:232) [businessobjects.jar:?]
	at psdi.iface.util.SecurityUtil.getUserInfo(SecurityUtil.java:86) [businessobjects.jar:?]
	at psdi.iface.action.MAXActionServiceBean.secureAction(MAXActionServiceBean.java:181) [mboejb.jar:?]
	at psdi.iface.action.MAXActionServiceBean.wsSecureAction(MAXActionServiceBean.java:204) [mboejb.jar:?]
	at psdi.iface.action.EJSLocalStatelessactionservice_05493ca6.wsSecureAction(Unknown Source) [mboejb.jar:?]
	at psdi.iface.webservices.ActionWebServiceProxy.invokeService(ActionWebServiceProxy.java:93) [classes/:?]
	at psdi.iface.webservices.JAXWSWebServiceProvider.invoke(JAXWSWebServiceProvider.java:149) [classes/:?]
It was followed by this error message:

BMXAA6713E - The MBO fetch operation failed in the mboset with the SQL error code 1008. The record could not be retrieved from the database. See the log file for more details about the error.
java.sql.SQLException: ORA-01008: not all variables bound

When using DB2, the stack trace looks like

BMXAA6714E - The data for the next record in the mboset could not be retrieved for the SQL query select * from maxuser  where loginid = :1 for read only. See the log file for more details about the error.
com.ibm.db2.jcc.am.SqlException: The number of variables in the EXECUTE statement, the number of variables in the OPEN statement, or the number of arguments in an OPEN statement for a parameterized cursor is not equal to the number of values required.. SQLCODE=-313, SQLSTATE=07004, DRIVER=3.69.71
	at com.ibm.db2.jcc.am.gd.a(gd.java:752)
	at com.ibm.db2.jcc.am.gd.a(gd.java:66)
	at com.ibm.db2.jcc.am.gd.a(gd.java:135)
	at com.ibm.db2.jcc.am.bp.c(bp.java:2780)
	at com.ibm.db2.jcc.am.bp.a(bp.java:2244)
	at com.ibm.db2.jcc.t4.bb.o(bb.java:906)
	at com.ibm.db2.jcc.t4.bb.j(bb.java:267)
	at com.ibm.db2.jcc.t4.bb.d(bb.java:55)
	at com.ibm.db2.jcc.t4.p.c(p.java:44)
	at com.ibm.db2.jcc.t4.vb.j(vb.java:157)
	at com.ibm.db2.jcc.am.bp.lb(bp.java:2239)
	at com.ibm.db2.jcc.am.bp.a(bp.java:3281)
	at com.ibm.db2.jcc.am.bp.a(bp.java:708)
	at com.ibm.db2.jcc.am.bp.executeQuery(bp.java:687)
	at psdi.mbo.MboSet.getNextRecordData(MboSet.java:3333)
	at psdi.mbo.MboSet.fetchMbosActual(MboSet.java:2941)
	at psdi.mbo.MboSet.fetchMbos(MboSet.java:2898)
	at psdi.mbo.MboSet.getMbo(MboSet.java:2114)
	at psdi.mbo.MboSet.isEmpty(MboSet.java:4410)
	at psdi.security.SecurityService.commonUserValidation(SecurityService.java:1328)
	at psdi.security.SecurityService.authenticateUserM(SecurityService.java:893)

That's a lot of stack traces. I've included them to make this blog entry easier to find in the future. 

So what was the problem? mxe.useAppServerSecurity was 0, mxe.int.allowdefaultlogin was 0, and the webservice call was not passing a MAXAUTH header. This scenario resulted in a null username which triggered the database error. 

In our case, changing mxe.int.allowdefaultlogin was our solution. Changing mxe.useAppServerSecurity or passing a MAXAUTH header would have also worked.

Wednesday, September 28, 2022

Preventing User Logins

Still Around

I have not written in a long time.  Until early this year, I have been supporting a client still running a highly customized Maximo 7.5.  There were some interesting problems come up, especially as the browsers remove support for Java.  Maximo 7.5 uses Java for Workflow editing and Direct Print.  I didn't think I had much to share as the world moved on to later versions of Maximo.

My new position has me working with Maximo 7.6 and MAS8.  I should have more to write about.

Preventing User Logins

I have recently been working on a problem where I want to prevent users from logging into a Maximo node after startup while some complex initialization takes place within the Maximo JVM.  I want the same behaviour as placing Maximo into Admin Mode without actually doing that.   This snippet of code will do that.

// grab the SECURITY service.
SecurityService securityService = (SecurityService)MaximoHelper.getService("SECURITY");

// disable user logins
((TenantLevelObj)securityService.getAllowNewSessions()).set(Boolean.FALSE);

// do what you want to do without user logins

// enable user logins
((TenantLevelObj)securityService.getAllowNewSessions()).set(Boolean.TRUE);
This will prevent new users from logging in but will not log out users who are currently logged in. 

Friday, November 23, 2018

Maximo Log Analysis Tool

This was originally posted on the Interloc blog.

I recently needed to diagnose out of memory problems with Maximo.  There is some information in the Maximo logs that can help.  Maximo can display mbo counts and free memory information.  Maximo will also log the number of active users and when crons execute.

If the mxe.mbocount system property is set to 1, then Maximo will output a count of each MboSet type and the total number of those Mbos in the system every minute.  It would look something like this:
PERSON: mbosets (275), mbos (550).

Also every minute, Maximo will display the number of users connected to each instance
BMXAA6369I - Server host: 192.168.1.10. Server name: UI03A. Number of users: 9
and the total amount of memory available and used in the JVM
BMXAA7019I - The total memory is 2684354560 and the memory available is 633519896.

All this is useful information, but it can be hard to make sense of it by just looking at it as text in the log files.  To help visualize it better, I created a web application to parse the logs and graph the data.  It also gives the option of downloading the raw data so you can analyze the data yourself.  The web application can be found at http://apps.thatmaximoguy.ca/maximologanalyzer/

To use it, upload a zip file containing SystemOut logs.

Once the file is loaded, specify an identifier to label the graph, the size you want the graphs, and click Process.  It assumes that sorting the log filenames will place them in chronological order.  This will be true if you are using the default SystemOut naming and log rotation.  If you upload files that do not start with SystemOut, you will be prompted how to parse the date and time from the log file.  This will follow Java’s SimpleDateFormat class.


After the log files have been processed, you can view the graphs in your browser or download them along with the extracted data.

In addition to information about memory usage, the Log Analyzer can graph the number of active users and when crons execute.

While analyzing some logs, I ran into an interesting phenomenon as seen in the following graph.

The total available is on a downward trend until each restart (black vertical line).  Initially I would have said that this is an example of a memory leak.  Identically configured sibling nodes did not show this trend.  Deeper analysis showed that this node had actually been taken out of the cluster and hadn’t done anything for weeks.  The sibling nodes that did process requests showed deeper drops in total available memory and higher peaks when memory was released.  My interpretation is that there is a “laziness” to garbage collection: the JVM will release the easy stuff but won’t look any harder than it needs to. 

Monday, May 28, 2018

Solving “Record has been updated by another user. Refetch and try again.” Problems

Originally posted at Interloc Solutions' Blog.

We’ve all seen the dreaded “Record has been updated by another user. Refetch and try again.” It can happen when the record has been updated by another user, but it can also happen when the record is updated by the same user more than once.

In memory, an MboSet owns zero or more Mbos. Each of those Mbos can have zero or more MboSets which in turn own zero or more Mbos. Naturally, if your MboSets contain zero Mbos you won’t have problems with records updated by another user. Problems will arise if two different Mbos reference the same database record and both Mbos attempt to update data. In memory, these Mbos will be represented as separate Java objects and will be owned by different MboSets. If only one Mbo is updated, it won’t be a problem. If both Mbos are updated, the first will update the database record and change the ROWSTAMP value. The second will attempt to update the record but will fail because the ROWSTAMP doesn’t match. This will trigger an MXRowUpdateException.

Let’s look at an example. In this diagram, ellipses represent Mbo objects in memory. Rectangular boxes represent database tables and their records. Solid lines represent an in-memory association through the named relationship. Dashed lines point to the database record.



In this example, a WORKORDER Mbo loads work order wo1. Following the woactivity relationship, WOACTIVITY wo1.2 is loaded into an Mbo. From here, the parent relationship is followed to WORKORDER wo1. The two WORKORDER objects reference the same database record, but they are represented in memory by two separate Mbo objects because they were loaded through two different relationships.

If either the first WORKORDER Mbo or the second WORKORDER Mbo is updated, it will save properly. If both Mbos are updated, the save will fail with an MXRowUpdateException. This occurs because the first update is applied to the database and updates the ROWSTAMP column. The second update is then applied, but the ROWSTAMP column has changed so it fails with an MXRowUpdateException.

To find where a single database record is updated by multiple Mbos, I have developed a MboDumper class. Starting from a single Mbo, it will travel down all instantiated relationships and display the relationship name and all Mbos contained there-in. If it detects the same Mbo in more than one place, it will highlight it. It’s then a matter of finding where in the code the Mbos are modified.

Code for the MboDumper is available on GitHub at https://github.com/ThatMaximoGuy/MboDumper.

To use the MboDumper, place a try-catch block around the call to save. Exactly where will depend on whether the save takes place in a DataBean or in a Cron Task or something else. The code for the above example could look like:
MXServer mxs = MXServer.getMXServer();
UserInfo ui = mxs.getSystemUserInfo();
MboSetRemote workorders = mxs.getMboSet("WORKORDER", ui);
workorders.setWhere("wonum = '10190548'");
MboRemote workorder = workorders.getMbo(0);
workorder.setValue("DESCRIPTION", "Example");

MboSetRemote woactivities = workorder.getMboSet("WOACTIVITY");
MboRemote woactivity = woactivities.getMbo(0);

MboSetRemote parents = woactivity.getMboSet("PARENT");
MboRemote parent = parents.getMbo(0);
parent.setValue("DESCRIPTION", "Example 2");
try {
   workorders.save();
} catch (MXRowUpdateException e) {
   MboDumper.dump(FixedLoggers.MAXIMOLOGGER, mbo);
   throw e;
}

Data is output to the given logger at the INFO level.

Given the example above, MboDumper would generate output like this:

*** MBO Data Dump Start
Mbo: WORKORDER [ToBeSaved ToBeUpdated] <=== DUPLICATE MBO
Key SITEID: EAGLENA
Key WONUM: 10190548
Modified DESCRIPTION: Example
Modified CHANGEBY: MAXADMIN
Modified CHANGEDATE: 2/27/18 3:03 PM
Relationship: WOACTIVITY parent= '10190548'  and siteid= 'AMTRKENG' 
Mbo: WOACTIVITY [ToBeSaved ToBeUpdated] 
Key SITEID: EAGLENA
Key WONUM: 10190549
Relationship: $SECGROUPS userid = 'MAXADMIN' and groupname in ('MAXADMIN','SUPERUSER')
Mbo: GROUPUSER [] 
Key USERID: MAXADMIN
Key GROUPNAME: MAXADMIN
Relationship: __LONGDESCRIPTIONMLFALSEWORKORDER ldownertable = 'WORKORDER' and ldkey =  196399 
Relationship: PARENT wonum= '10190548'  and siteid= 'AMTRKENG' 
Mbo: WORKORDER [ToBeSaved ToBeUpdated ] <=== DUPLICATE MBO
Key SITEID: EAGLENA
Key WONUM: 10190548
Modified DESCRIPTION: Example 2
Modified CHANGEBY: MAXADMIN
Modified CHANGEDATE: 2/27/18 3:03 PM
Relationship: CLASSSTRUCTURE classstructureid =  '' 
Relationship: __LONGDESCRIPTIONMLFALSEWORKORDER ldownertable = 'WORKORDER' and ldkey =  196398 
Relationship: CLASSSTRUCTURE classstructureid =  '' 
Relationship: __LONGDESCRIPTIONMLFALSEWORKORDER ldownertable = 'WORKORDER' and ldkey =  196398 
*** MBO Data Dump Done

The WORKORDER updated in two places is marked with “<== DUPLICATE MBO.“

Given the Mbo and the relationship name, it’s usually enough to find where in the code these multiple updates occur.

Thursday, March 29, 2018

Using Ant to Deploy Automation Scripts

Introduction

I am a firm believer in deploying from source control.  The only thing that should end up in a production environment should come from the source control system.  This is the best way of knowing what is in production and the best way to know that your source is up to date.

It's relatively easy to set up a process to deploy Java changes to Maximo from code taken from the source control system.  Automation scripting makes this harder.  Scripts can be written from within Maximo and some cut & paste process can be used to copy changes to a file that's under source control with Migration Manager moving the changes from one environment to another.  The problem is making sure the script in Maximo is the same as the one in source control.  Any manual process to keep the two files in sync will be error prone and Migration Manager will only propagate that error through the environments.

Using an ant script and some Enterprise Services, I have managed to automate deploying Automation scripts to Maximo.

Create External System

In Maximo, create an External System.  I called mine CONFIG.  Make it use the MXXMLFILE endpoint, or create your own XML file endpoint.

Duplicate the DMLAUNCHPOINT and DMSCRIPT Obejct Structures.  I called mine IS_LAUNCHPOINT and IS_SCRIPT.

Create an Enterprise Service for IS_LAUNCHPOINT called ConfigLAUNCHPOINT and another one for IS_SCRIPT called ConfigSCRIPT.  

Also, create Publish Channels for IS_LAUNCHPOINT and IS_SCRIPT.  This makes it easier to extract data.

Associate the Publish Channel and the Enterprise Service created above with the External System created above.  Make sure everything is enabled.

Ant Script

Here is the ant script I created to publish xml files to the Enterprise Services.  You'll need ant-contrib, commons-httpclient, commons-logging and commons-codec jar files.

<project name="DeployXML" default="deploy" basedir=".">
    <description>Deploy Maximo XML files through Enterprise Services</description>

    <taskdef resource="net/sf/antcontrib/antlib.xml" onerror="fail">
        <classpath>
            <pathelement location="ant-contrib.jar"/>
            <pathelement location="commons-httpclient-3.0.1.jar"/>
            <pathelement location="commons-logging-1.0.4.jar"/>
            <pathelement location="commons-codec-1.10.jar"/>
        </classpath>
    </taskdef>

    <property name="deploy.file" value="deploy.properties" />

    <target name="deploy" depends="init" description="Deploy XML files to Maximo.  The files are determined from deploy properties file.">
        <for list="${deploy.list}" param="index">
            <sequential>
                <propertycopy name="temp.name" from="deploy.@{index}.name" override="true"/>
                <propertycopy name="temp.dir" from="deploy.@{index}.dir" override="true"/>
                <propertycopy name="temp.files" from="deploy.@{index}.files" override="true"/>
                <propertycopy name="temp.url" from="deploy.@{index}.url" override="true"/>
                <echo message="Processing @{index} ${temp.name}"/>
                <antcall target="deploy.directory">
                    <param name="service.name" value="${temp.name}"/>
                    <param name="service.dir" value="${temp.dir}"/>
                    <param name="service.files" value="${temp.files}"/>
                    <param name="service.url" value="${temp.url}"/>
                </antcall>                
            </sequential>
        </for>
    </target>

    <target name="deploy.directory">        
        <echo message="Processing ${service.name} ${service.dir}/${service.files}" />
        <for param="file">
            <path>
                <fileset dir="${service.dir}">
                    <filename name="${service.files}"/>
                </fileset>
            </path>
            <sequential>
                <antcall target="deploy.single">
                    <param name="file" value="@{file}"/>
                </antcall>
            </sequential>
        </for>
    </target>
    
    <target name="deploy.single" depends="init.client">
        <echo message="Deploying ${file}"/>
        <postMethod url="${service.url}"  responseDataProperty="response" clientRefId="maximo">
            <file path="${file}" contentType="text/xml;charset=utf-8"/>            
        </postMethod>
        <echo message="${response}"/>
    </target>   

    <target name="init">
        <loadproperties srcFile="${deploy.file}" />
    </target>
    
    <target name="init.client">
        <httpClient id="maximo">
            <clientParams authenticationPreemptive="true"></clientParams>        
            <httpState>
                <credentials username="${maximo.username}" password="${maximo.password}"/>
            </httpState>
        </httpClient>
    </target>
</project>

Property File

Here is the property file.  

maximo.base.url=http://localhost:9080/meaweb/esqueue/CONFIG
maximo.base.directory=C:/Users/developer/workspace/Maximo
maximo.username=username
maximo.password=password

deploy.list=1,2

deploy.1.name=Script
deploy.1.dir=${maximo.base.directory}/Config/
deploy.1.files=**/*ConfigSCRIPT*.xml
deploy.1.url=${maximo.base.url}/IS_ConfigSCRIPT

deploy.2.name=LaunchPoint
deploy.2.dir=${maximo.base.directory}/Config/
deploy.2.files=**/*ConfigLAUNCHPOINT*.xml
deploy.2.url=${maximo.base.url}/IS_ConfigLAUNCHPOINT

Set maximo.base.url to the root url of all Enterprise Services for the External System you created.

Set maximo.username and maximo.password either in the property file or as command line parameters.  This will be a username and password that is allowed to call the Enterprise Services.

Set maximo.base.directory to the top level of your source project.

Set deploy.list to the list of configurations you want deployed.  They will be deployed in the order given.  Each entry is an index to the deploy.x entries below.

Set deploy.x.name to a descriptive name of what will be deployed.  It will be displayed while the ant script runes.

Set deploy.x.dir to a directory in which the files to upload can be found.  This can be a top level directory as shown here.  It really depends on how you want to organize your files.

Set deploy.x.files to a file or files to import into Maximo.  The sample property file will actually look in all subdirectories for file names that contain ConfigScript.  Again, this will depend on how your files are organized.

Set deploy.x.url to the URL of the Enterprise Service that will import the files.

Deploying Automation Scripts

Deploying the automation scripts is simply a matter of running the ant script. 

Once the ant script completes, it's a good idea to check Message Reprocessing in case there were any errors importing the scripts.

You can use this to also deploy workflows.

Friday, January 19, 2018

Risk Assessment Tool


I originally posted this at Interloc Solution's blog.

At my current client, we use a Change Request approach to Maximo changes.  A Change Request is created describing the new functionality desired.  Developers work on these changes in separate Subversion branches.  Change Requests are chosen for a Release, then merged together, tested, and deployed.  The merged changes are then merged back into our Trunk and the process repeats.  Change Requests are not necessarily deployed during the next Release.  It can be several Releases before a Change Request is deployed.

When it is decided to include a Change in a Release, a risk assessment is performed.  Two common questions are “What has this change touched,” and “Does it require regression testing.”  The intent is not to get into the details of the change, but to provide a rough overview that can help QA get an idea of the scope of the change.

We have recently introduced a Risk Assessment Tool modeled on the FAA’s Flight Risk Assessment Tool.  It is available on GitHub.  We are looking for a better acronym than MRAT or RAT when referring to this tool.  I welcome any input on the matter.

It is simply a list of development activities that are weighted depending on their potential impact.



Once development on a Change Request is complete, the developer creates a copy of the Risk Assessment Tool specific for their change.  The developer goes through and places a “1” in the Applicable column for anything that applies.  An overall score for the page is calculated automatically.

The development activities are then grouped together into more general types of changes and provides an overall score.

The end product is an overall summary of what has been changed (e.g. Screen, Database, Coding, etc.) and a score that gives an idea of the size of the change.  A bigger number means a bigger change means bigger risk and implies more testing.

The Considerations column contains notes to the developer to check common mistakes associated with a change.  For example, a consideration for adding a database column that is part of an Object Structure is that it has the potential of affecting external systems that consume that Object Structure.  The consideration column also contains notes to QA about what should be tested.  When an Mbo save method is modified, the Consideration is that testing should include saving a record.

The Score calculation is simply Weight × Applicable.  The common usage is to place a “1” in the Applicable column.  In the beginning we toyed around with the idea of using larger numbers to represent larger changes.  For example, if two Mbo classes were modified, then place a “2” in the Applicable column.  We felt this had the potential of overweighting changes.  We decided to go with ranges — 1 file, 2 to 5 files, etc — each with a different weight.

Where the FAA Flight Risk Assessment Tool gives meaning to the calculated scores — 0-10 Not Complex Flight, 11-20 Exercise Caution, 20-30 Area of Concern — we haven’t yet determined appropriate ranges and what those ranges might mean, so any outside thoughts or input would be welcome.
There are some obvious areas that are missing from this tool, such as Work Flow.  We don’t use it, so we don’t have a section for it. So any thoughts or input would be welcome here as well.

The Risk Assessment Tool is a simple approach that gives an overview of where changes took place and how much of an impact they might have.

Custom Start Center Portlets

I originally posted at Interloc Solution's blog.


Maximo's Start Center is populated with a number of different portlets.  They provide KPI data, Favorite links, the user's Inbox, among other things.  While it is possible to create custom portlets in Maximo, out of the box functionality does not permit saving them in a Start Center template.

Having looked into how Start Center templates are managed, I have created some extensions that permit saving custom portlets in a Start Center template.  Using these customizations "simplifies" custom portlet design to 10 easy steps.  They are:
  1. Modify the SCCONFIG service in Maximo with ISStartCenterService.
  2. Create an application for the portlet with READ and NOPORTLET signature options for it.
  3. Create a new service class that implements CustomPortletHandler.
  4. Create a data table to store portlet settings.
  5. Create a definition in the PORTLET table.
  6. Create a portlet data class for the jsp presentation file.
  7. Create a jsp file to display the portlet.
  8. Register the new portlet jsp.
  9. Create a DataBean for the configuration screen.
  10. Create a presentation screen for modifying portlet data.
What follows will be a breakdown of the steps listed above.  You can follow along with the MaximoIFramePortlet, a sample portlet that places an IFrame in the Maximo Start Centre.  The source is available on GitHub at https://github.com/ThatMaximoGuy/MaximoIFramePortlet.

Modify SCCONFIG

Refer to MaximoIFramePortlet/tools/maximo/en/isiframe/iframe_001.dbc

Modify the classname of the SCCONFIG service in Maximo to com.interlocsolutions.maximo.app.scconfig.ISStartCenterService.  The class can be found in the Interloc Solutions MaximoPortlet.jar.
The SCCONFIG service handles saving and loading Start Center templates.  An investigation of the base class shows that all the out of the box portlets are hard coded into the service without any extension points to handle custom portlets.  

The ISStartCenterService implements an extension mechanism to work with any number of custom portlets without any additional modifications to the service class.

The ISStartCenterService will identify any Maximo services that implement the CustomPortletHandler interface. When it loads or saves a Start Center template, it will identify any custom portlets and delegate loading and saving of the porlet data to the correct service. This permits creating many different custom portlets without any additional modifications to ISStartCenterService.

Create an Application for the Portlet

Refer to MaximoIFramePortlet/tools/maximo/en/isiframe/iframe_001.dbc

Create an application definition in MAXAPPS for the portlet.  It must have READ and NOPORTLET sigoptions.  The main table name for the new application will be the data table created later.  The data table will hold the user settings for the portlet.

In the MaximoIFramePortlet, an additional sigoption is created to limit the user’s ability to change the url assigned to the IFrame.

Create a New Service Class

Refer toMaximoIFramePortlet/src-mbo/com/interlocsolutions/maximo/app/iframe/ISIFrameService.java

Create a new service class that implements com.interlocsolutions.maximo.app.scconfig.CustomPortletHandler.  The CustomPortletHandler interface marks the service as able to save and load a custom portlet.  The methods the interface provides will do the work of saving and loading the portlet.

The methods xmlNodeName and portletName identify which portlet the service is responsible for managing.  The xmlNodeName is the name found in the Start Center Template XML.  The portletName is the name found in the SCCONFIG database table.

The loadPortlet method is called when the portlet is loaded from the SCTEMPLATE XML.  The method is called with a reference to the XML node from the Start Center Template that represents the definition for the portlet.  It also receives a reference to a class that is doing most of the work of loading the portlet.  If you have a layout with any child elements in the XML, the copyContentUId has to be called for each child element.  The last parameter is a reference to the LAYOUT Mbo that is the parent for any of the portlet data.
In the MaximoIFramePortlet, the loadPortlet is responsible for loading the URL and IFrame window size from the XML and populating it in a data table in the database.

The savePortlet method is called when the portlet is saved to the SCTEMPLATE XML.  It has the same parameters as loadPortlet, but the data travels from the database to XML.  It is responsible for creating the XML nodes that will be saved in the Start Center Template.
In the MaximoIFramePortlet, the savePortlet creates the XML node for the MaximoIFramePortlet and sets the URL and window sizes as attributes of the XML node.

Refer toMaximoIFramePortlet/tools/maximo/en/isiframe/iframe_001.dbc

Register this new class with Maximo.

Create a Data Table

Refer to MaximoIFramePortlet/tools/maximo/en/isiframe/iframe_001.dbc

Create a data table to store portlet settings. It must have a LAYOUTID field the same as LAYOUT.LAYOUTID. It must also have a relationship defined between LAYOUT and itself. 

Refer toMaximoIFramePortlet/src-mbo/com/interlocsolutions/maximo/app/iframe/ISIFrameCfgSet.java
MaximoIFramePortlet/src-mbo/com/interlocsolutions/maximo/app/iframe/ISIFrameCfg.java

Create and register an MboSet for the data table so that the layoutid of the data table is populated from the owner when a record is created.  If the Mbo extends from com.interlocsolutions.maximo.app.scconfig.AbstractPortletConfig, then it will handle that properly.

In MaximoIFramePortlet, the data table contains the URL to display in the IFrame and its height and width.

Create a Portlet Definition

Refer to MaximoIFramePortlet/tools/maximo/en/isiframe/iframe_001.dbc

Create a definition in the PORTLET table. The PORTLETID should match the value returned from CustomPortletHandler.portletName().  This will be displayed in the Available Portlets list in the Start Center Layout and Configuration screen.

In MaximoIFramePortlet, the portlet name is going to be called ISIFRAME and it will be a Wide portlet.

Create a Portlet Data Class

Refer to MaximoIFramePortlet/src-ui/com/interlocsolutions/maximo/webclient/iframe/IFramePortlet.java

Create a class that extends from psdi.webclient.controls.PortletDataInstance. This class will provide data access methods to the underlying data for the jsp presentation file.  Basically this class is responsible for taking the data from the Mbo and formatting it for the jsp file.  It should have a get method for each data column defined in the data table earlier.

In MaximoIFramePortlet, the IFramePortlet class defines methods to retrieve the URL, the IFrame height, and its width.

Create a JSP File

Refer to MaximoIFramePortlet/applications/maximo/maximouiweb/webmodule/webclient/components/isiframeportlet.jsp

The jsp file is what finally displays in the Start Center.  A lot of it is boiler plate.  It is most obvious where to add the custom code by referring to isiframeportlet.jsp.  Where that file has an iframe tag is where the HTML to display the portlet should go.  It’s simply standard HTML and JSP coding.  The filename should match the CONTROLNAME column from the PORTLET table definition.

In MaximoFramePortlet, there is an HTML iframe tag which takes its src, height, and width attributes from the Portlet Data Class.

Register the Portlet

Refer to MaximoIFramePortlet/src-tools/component-registry.fragment.xml
MaximoIFramePortlet/src-tools/components.fragment.xsd
MaximoIFramePortlet/src-tools/control-registry.fragment.xml
MaximoIFramePortlet/src-tools/presentation.fragment.xsd

For Maximo to know how to display the portlet, it needs to be registered as a component.  For that, entries in component-register.xml, components.xsd, control-registry.xml, and presentation.xsd must be created.

Start by copying the definitions from another portlet.  And make the following changes:
  • Component-registry.xml:
    1. Set the component-description name attribute to the control name of the portlet. This value will match the CONTROLNAME defined in the PORTLET table and the name of the jsp file without the extension.
    2. Set the default-value for the jsp-filename property to the name of the jsp file, without the extension.
  • Components.xsd:
    1. Set the xsd:element name attribute to match the CONTROLNAME defined in the PORTLET table. It has to match the component-description name used in the component-registry.xml.
    2. Set the default attribute of the jsp-filename property to the name of the jsp file, without extension.
  • Control-registry.xml:
    1. Set the control-descriptor name attribute to the control name of the portlet.
    2. Set the control-descriptor instance-class attribute to the fully qualified Java class name of the Portlet Data Class.
    3. Set the default-value for the relationship property to the relationship created to map from the LAYOUT table to the Data Table.
  • Presentation.xsd:
    1. Set the xsd:element name attribute to match the CONTROLNAME defined in the PORTLET table. It has to match the control-descriptor name used in the control-registry.xml.
    2. Set the default attribute of the relationship property to the relationship created to map from the LAYOUT table to the Data Table.
Refer to MaximoIFramePortlet/src-tools/com/interlocsolutions/maximo/tools/IFramePortletEnvSetup.java

MaximoIFramePortlet has a standalone application that will update the registry files.

Create a Portlet Configuration DataBean

Refer to /MaximoIFramePortlet/src-ui/com/interlocsolutions/maximo/webclient/iframe/IFramePortletBean.java

The Portlet Configuration DataBean will support the portlet configuration screen. The portlet configuration screen is the screen displayed when the “Edit Portlet” icon is clicked.  The bean has to ensure an Mbo exists to capture configuration data.

In MaximoIFramePortlet, the DataBean creates a new ISIFRAMECFG record if one doesn’t already exist.

Create a Presentation Screen

Refer to /MaximoIFramePortlet/tools/maximo/en/isiframe/isiframe.xml
The final step is to create a presentation for the portlet configuration screen.  This is done like other Maximo applications.  The beanclass for the application will be the Portlet Configuration DataBean created earlier.  The main object will be LAYOUT.  On the screen, create fields to capture all the configuration data required for the portlet. 

In MaximoIFramePortlet, there are three fields for the URL, width, and height.  The URL field is controlled the MODIFYURL sigoption.  This simply limits who can change the IFrame URL.

The Final Result

The final result is a custom Maximo Start Center Portlet.