Have you ever used a variable and received this error? Error: Unable to retrieve variable [variable name] deployed in the application [app name] Rule [app name].[plan type name].[rule name]. You likely saw this when a Groovy variable was used inside of {}. Read more
I am currently working on a migration of on-premise to cloud project (going GREAT by the way). One of the things we are working on is the change with the data integration. One of the processes loads the number of working days to the application from a source that provides it. “Why not use Groovy,” I ask? It turns out to be a great question. Read more
Add complex calculations in ASO without changing or adding to the existing architecture. Groovy calculations enable improved performance and reduce complexity of calculation logic by bypassing Essbase. Taking advantage of new technology and adding creativity reduced the development timeline significantly.
Check out this success story. Download the printable version.
I can’t tell you how many times I have been at a client and they wanted to replicate eliminations at the first common parent like HFM, or some other consolidations system. Maybe there is a good way to do this, but I could never find an efficient and effective way to accomplish it with Essbase calculations. Well, that is history. This is yet another example of how to solve an historically complex problem very simply. Read more
I had a great question today so challenge accepted! A reader asked if it was possible to run a data export and have the system date in the file name. The answer is very simply, yes. I don’t have any content around the question, so I will answer it in two ways. Read more
To date, we have talked about the performance improvements Groovy introduces, as well as the creative validation we can add. One thing that hasn’t been covered yet is the ability to add functionality to make the input easier for a planner. Replicating changes through the months, resetting the values back to the defaults, and many other concepts can be developed to make the user’s lives easier. Read more
Manipulating dates is not something you may think is that useful, but there are a number of things that we have done in the past that are slow performing in Essbase, and others that were not possible or you may not have ever thought of. This is going to walk visitors through ways to manipulate dates for all kinds of uses. Hopefully it inspires some ideas for you to make your application a little more polished. Read more
There has not been alot of troubleshooting discussed in the adventures series. Just like with most coding languages, you can gracefully handle errors resulting from actions (like divide by 0) and return descriptive information to the users and administrators in the job console. There are several benefits that I see.
- As previously stated, since the error is accounted for, the user doesn’t get a message that shows a failure with no context.
- The error object will provide more information about what happened and what should be done to fix it in the future.
- Predefined actions can take place since the error doesn’t interrupt the script, like returning an error message that tells the user to contact the administrator with an action
Error Handling Introduction
Try / catch / finally is a concept most development languages have. Conceptually, you “try” some group of commands and “catch” any errors that might happen. If you “catch” an error, you account for it by doing something. “Finally,” you perform any closing actions.
try { def arr = 1/0 } catch(Exception ex) { println ex.toString() println ex.getMessage() println ex.getStackTrace() }finally { println "The final block" }
In this case, ex.toString() prints
java.lang.ArithmeticException: Division by zero
ex.getMessage() prints
Division by zero
and ex.getStackTrace()
[java.math.BigDecimal.divide(Unknown Source), org.codehaus.groovy.runtime.typehandling.BigDecimalMath.divideImpl(BigDecimalMath.java:68), org.codehaus.groovy.runtime.typehandling.IntegerMath.divideImpl(IntegerMath.java:49), org.codehaus.groovy.runtime.dgmimpl.NumberNumberDiv$NumberNumber.invoke(NumberNumberDiv.java:323), org.codehaus.groovy.runtime.callsite.PojoMetaMethodSite.call(PojoMetaMethodSite.java:56), org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48), org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:113), org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:125), ConsoleScript11.run(ConsoleScript11:2), groovy.lang.GroovyShell.runScriptOrMainOrTestOrRunnable(GroovyShell.java:263), groovy.lang.GroovyShell.run(GroovyShell.java:387), groovy.lang.GroovyShell.run(GroovyShell.java:366), groovy.lang.GroovyShell.run(GroovyShell.java:170), groovy.lang.GroovyShell$run$0.call(Unknown Source), groovy.ui.Console$_runScriptImpl_closure18.doCall(Console.groovy:1123), groovy.ui.Console$_runScriptImpl_closure18.doCall(Console.groovy), sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method), sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source), sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source), java.lang.reflect.Method.invoke(Unknown Source), org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:98), groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:325), org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:294), groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:989), groovy.lang.Closure.call(Closure.java:415), groovy.lang.Closure.call(Closure.java:409), groovy.lang.Closure.run(Closure.java:496), java.lang.Thread.run(Unknown Source)]
The script in the final block is also written out. It is intended for cleanup and tasks that run at the end of a script regardless of whether there is an error or not.
Handling Errors Uniquely
The catch command can be replicated to handle errors uniquely. Let’s expand on the example above. Assume the variable is coming from an RTP or cell value. The following has a catch for a specific error. The java.lang.ArithmeticException is equal to the output of ex.toString(). There are probably thousands of errors, if not more. The easiest way for me to grab these is to use the ex.toString() and see what it produces. I have no desire to remember or learn them all!
The following will do something different for the divide by zero error than all other errors.
try { def denominator = 0 println 1/denominator } catch(java.lang.ArithmeticException ex) { println ex.getMessage() println "an action should be taken to account for the error" } catch(Exception ex) { println ex.toString() println ex.getMessage() println ex.getStackTrace() } finally { println "The final block" }
Finishing Up
This requires a little more effort, but once you get used to doing this, it can be reused. I hear the argument that if you account for every possible situation, you don’t need to error trap. That is true, and if you are smart enough to account for everything that can go wrong, don’t include this in your Groovy calculation. I would argue that simple calculations probably don’t need this level of error handling, but more complex logic might be a candidate. The example above could obviously be handled with an if statement, but put it in context. It is used to illustrate the concept of try / catch / finally.
I was fortunate enough to speak for ODTUG a few weeks ago and really excited that my discussions around Groovy are getting some of the most attended and most interactive ODTUG webinars. If you have put any of these presentations together, you know how much time it takes to do the research, consolidate the information, make it presentable, and spend the time to hopefully make it fluid. So, when you provide feedback, I really appreciate it.
The Webinar
There were some questions I wasn’t able to answer, so here goes.
Can the dataCellIterator take functions like @IDESC, @CHLIDREN, etc?
The iterator iterates through the grid, so it doesn’t have the ability to do this directly. I am not sure of the question, but you could iterate through the grid and for each cell use these functions in other classes/methods to do things like see if it has children, or check to see if it is a child of something.
Is there a way to have a groovy business rule to call a non-groovy business rule – for example if cells were edited then run BR1 else return?
Yes and no. There is no way to execute another rule. But, you can embed it into the script like you can in any other rule or script. You can neither write the string or drag and drop the rule into the Groovy rule. It doesn’t always put it where the cursor is, but you can cut and paste it to wherever you need it. It basically is like an include and just embeds the script text, so it would need to be in a string builder.
Can We improve the Aggregations with Groovy?
Yes and no. If you dynamically create an aggregation script that is the exact same as a normal rule, the same time would result. Where you do get a benefit is that you can only consolidate the impacted members and dimensions based on what has been edited. You can also move the data from the BSO to the ASO cube and eliminate the need to aggregate, which would obviously improve the perception of speed.
Does groovy interact with workflow, valid intersection, copying attached documents / supporting detail?
Workflow is in the roadmap. I confirmed last week with development, so it is coming. Attachments and supporting detail can be copied by executing smart pushes.
Can grids be generated on the fly using groovy?
They sure can, but they aren’t visible to the user. There are two grid builders for retrieving and submitting data.
Is there any documentation available to give performance comparisons between business rule/calc and groovy?
Not that I know of, but as previously stated, Groovy doesn’t make Essbase faster. The perception to users will be that it does, but it is only because we have the ability to isolate what we calculate more than we did before. That said, if you use the grid builders to do the calculations and submit the results rather than use BSO calculations, you might see different results. There are some things (allocations) that I think is faster in BSO. I think using the grid builders on ASO – I do see improvements in performance using Groovy over procedural calculations. But, I want to emphasize that the majority of the time the speed is improved because of the ability to calculate only what we need to.
Groovy collections are used all throughout the ePBCS API. If you are not familiar with collections, you may want to take a look at Adventures in Groovy – Part 27: Understanding Collections before you continue. Maps, which are a type of collection, are very useful when moving data between different applications that have different member names representing the same data. In a the example below, data is moving from a product revenue cube to a financial cube. In the detailed cube, the member names are more descriptive, like Net Sales. In the financial application, the same data is a true account number from the GL, and names 42001. Mapping data between these two can easily be done with Groovy maps.
Introduction
There are two components to understanding the use of these maps. First, the map must be defined for use. The construction of the map is a delimited list of items. Each of the items is made up of an key and a value. These are separated by a colon.
//set account map def acctMap = ['Units':'Units', '42001-Product Sales':'Net Sales', '50001-Cost of Sales':'COGS', '50015-Write-offs':'Write-offs', '56010-Chargebacks':'Customer Satisfaction Discount', '50010-Sales and Discounts':'Sales and Discounts', '56055-Overstock Discount':'Overstock Discount', '56300-Customer Satisfaction Discount':'Customer Satisfaction Discount', '56092-Multi-Purchase Discount':'Multi-Purchase Discount', '56230-Open Box Discount':'Open Box Discount', '56200-Damage Container Discount':'Damage Container Discount', '56205-Damaged Box Discount':'Damaged Box Discount', '56090-Group Purchase Discount':'Group Purchase Discount']
The second piece is retrieving the mapped value. The value on the left of the colon is referenced and the value on the right will be returned. The following would return 56230.
[acctMap.get("56230-Open Box Discount")]
A fully vetted example follows of moving data from one database to several others. The function’s use is embedded in a loop, so rather than a hard coded value, the member of the account dimension is used as the accounts (rows in the form) are being iterated. It looks like this.
[acctMap.get(it.getMemberName('Account'))]
Working Use Case
The map above is used in several places for several reasons. First, the map is created. Second, the map is iterated and the key is used to create a data grid for all the values that will be copied, or synchronized, to the destination cube. Third, the map is used to lookup the converted value to create the grid connected to the destination. this is a complete working example. The items in red are specific to the map and its use.
//Dimension employeeDim = operation.application.getDimension("Account") //**************************************************************************** // Data Movement between Apps //**************************************************************************** // Get POV String sCompany = operation.grid.getCellWithMembers().getMemberName("Company") def sMaterialGroup = operation.grid.getCellWithMembers().getMemberName("Material_Group") String sChannel = operation.grid.getCellWithMembers().getMemberName("Channel") def lstProducts = [] operation.grid.dataCellIterator({DataCell cell -> cell.edited}).each{ lstProducts.add(it.getMemberName("Product")) } String strProducts = """\"${lstProducts.unique().join('","')}\"""" println "data push running for " + strProducts if(operation.grid.hasSmartPush("Prod_SmartPush") && lstProducts) operation.grid.getSmartPush("Prod_SmartPush").execute(["Product":strProducts,"Currency":'"USD","Local"']) //set account map def acctMap = ['Units':'Units', '42001-Product Sales':'Net Sales', '50001-Cost of Sales':'COGS', '50015-Write-offs':'Write-offs', '56010-Chargebacks':'Customer Satisfaction Discount', '50010-Sales and Discounts':'Sales and Discounts', '56055-Overstock Discount':'Overstock Discount', '56300-Customer Satisfaction Discount':'Customer Satisfaction Discount', '56092-Multi-Purchase Discount':'Multi-Purchase Discount', '56230-Open Box Discount':'Open Box Discount', '56200-Damage Container Discount':'Damage Container Discount', '56205-Damaged Box Discount':'Damaged Box Discount', '56090-Group Purchase Discount':'Group Purchase Discount'] Cube lookupCube = operation.application.getCube("rProd") DataGridDefinitionBuilder builder = lookupCube.dataGridDefinitionBuilder() builder.addPov(['Years', 'Scenario', 'Currency', 'Version', 'Company','Store_Type','Department','Source','Product','View'], [['&v_PlanYear'], ['OEP_Plan'], ['Local'], ['OEP_Working'], [sCompany],['Store_Type'],['Total_Department'],['Tot_Source'],['Tot_Product'],['MTD']]) builder.addColumn(['Period'], [ ['ILvl0Descendants("YearTotal")'] ]) for ( e in acctMap ) { builder.addRow(['Account'], [ [e.key] ]) } DataGridDefinition gridDefinition = builder.build() // Load the data grid from the lookup cube DataGrid dataGrid = lookupCube.loadGrid(gridDefinition, false) def povmbrs = dataGrid.pov def rowmbrs = dataGrid.rows def colmbrs = dataGrid.columns def tmpColMbrs = [] //Fin Grid Setup Cube finCube = operation.application.getCube("Fin") Cube rfinCube = operation.application.getCube("rFin") DataGridBuilder finGrid = finCube.dataGridBuilder("MM/DD/YYYY") DataGridBuilder rfinGrid = rfinCube.dataGridBuilder("MM/DD/YYYY") finGrid.addPov('&v_PlanYear','OEP_Plan','Local','OEP_Working',sCompany,'Prod_Model') rfinGrid.addPov('&v_PlanYear','OEP_Plan','Local','OEP_Working',sCompany,'Prod_Model','MTD') def colnames = colmbrs[0]*.essbaseMbrName String scolmbrs = "'" + colnames.join("', '") + "'" finGrid.addColumn(colmbrs[0]*.essbaseMbrName as String[]) rfinGrid.addColumn(colmbrs[0]*.essbaseMbrName as String[]) dataGrid.dataCellIterator('Jan').each{ it -> def sAcct = "${acctMap.get(it.getMemberName('Account'))}" def sValues = [] List addcells = new ArrayList() colmbrs[0].each{cName -> sValues.add(it.crossDimCell(cName.essbaseMbrName).data) addcells << it.crossDimCell(cName.essbaseMbrName).data } finGrid.addRow([acctMap.get(it.getMemberName('Account'))],addcells) rfinGrid.addRow([acctMap.get(it.getMemberName('Account'))],addcells) } DataGridBuilder.Status status = new DataGridBuilder.Status() DataGridBuilder.Status rstatus = new DataGridBuilder.Status() DataGrid grid = finGrid.build(status) DataGrid rgrid = rfinGrid.build(rstatus) finCube.saveGrid(grid) rfinCube.saveGrid(rgrid)
Finishing Up
This is a relatively simple concept and not terribly difficult to implement. It is also something most will benefit from when synchronizing data with the dataGridBuilder. Have something to add? Post a comment and I will get back to you promptly.