/ Development  

Shocking betrayal; Unearth disturbing history data after an entity instance disappears

Hi there AppWorks fans,

Welcome to a new installment of AppWorks tips.

A colleague asked me the following question:

1
2
3
4
5
6
7
8
9
Hey Antal...quick question
If you use the 'History' building block
And you throw away the instance
You can then enable on the 'History' BB to keep the history
But how do you get to that instance history?
That inflated content is saved somewhere in the database...Unreadable
And you can't open the entity instance to hit the 'View log' action button...
Never thought about it :)
Do you know?

Great question…There is not something like a separate ‘History viewer’ artifact; Or it should fall under the ‘Audit viewer’ artifact, but I don’t think so!? Interesting…


Let get right into it…

Time to boot up our beloved VM first and dive into our favorite project. Create a simple entity project with one basic prj_name property; generate all the default BBs. Make it nice and naming-wise-shiny, add the ‘History’ BB, and finish with a view like this (incl. those red-arrowed settings):

history_001

The history hl_project log is created with this input:
history_002

Save it, publish it, and let’s have a quick view in runtime:

history_003

Great!, but where is it saved? Well, I could only find one table in the database matching my criteria; it’s named o2aw_tipsgenerichistoryloglist!

history_004

Based on the settings of the ‘History’ BB this table will be retained with the entity data, will be cleaned on instance deletion, or will be noted as item deleted (this is cleaning the data and leaving just one row in the database!)

My next question would be: How is it received? For this I did a check in the developer tools of Chrome to conclude it’s just an HTTP GET request with a URL like this:

1
http://192.168.56.107:8080/home/appworks_tips/app/entityRestService/Items(080027f7a5dca1eea76a2f7890239f55.1)/History?language=en-US&historyEventDetails=Brief

When I look a little further (based on my experience), I can conclude the Typed ReST layer of the platform (since version 23.4) behind ‘/app/admin’ also has an operation to receive the details of this table.

history_005

Open it, and find yourself an operation with the logic name ViewHistory. Try it out with the entity ID as an input to get the same result:

history_006

Great, but do these HTTP calls still work when the instance in deleted from runtime? Well, as long as the ‘Retain History’ option is selected it should be fine; I just tried it out, and it’s indeed still working. This is the proof of a ‘Delete’ action which is saved in the history:

history_007

The other URL (with ‘/entityRestService’ in the path) is NOT working anymore! Sounds logic to me as this goes straight through the instance that was just removed!

Before we continue, I just wanted to play around quickly with 2 use-cases:

  1. ‘Retain history’, create instance in runtime, switch to ‘Only note deletion’, publish, delete the instance in runtime; This leaves us with only one row (the deletion note!). The old data is still there which is logic as we execute this on the instance.
  2. Switch to ‘Discard history’, publish, create instance in runtime, and delete it; Guess what? The old history items are still there (as expected), the history of the deleted instance is removed.

Conclusion (just for myself to write it off): Changing this setting afterward only has impact on the current entity at that moment the setting is packaged and delivered. Old history data is untouched! We can even clean up with a delete statement like this (where s_top_item_id is the ItemId of the entity instance):

1
2
DELETE FROM o2aw_tipsgenerichistoryloglist 
WHERE s_top_item_id = '080027f7a5dca1eea76a2f7890239f55.327683';

When (after this delete) the entity instance is changed, the history is filled up with data again!…Good to know!


The database encryption part

In the previous section, we saw a table column passing by called s_history_data with values starting with \x...; I was just wondering if we can decode this back into normal text? The column is of type ‘BYTEA’ in my Postgres database. A quick double-check from my friend ChatGPT on prompt: How can I convert bytea text to string?; It provided me with a nice function in this statement:

1
2
3
SELECT convert_from(s_history_data, 'UTF8') AS s_history_data 
FROM o2aw_tipsgenerichistoryloglist
WHERE s_top_item_id = '080027f7a5dca1eea76a2f7890239f55.1';

history_008

Time from some code-decryption (see the screenshot!):

Code Instruction Example
CD CreateDraft Draft 'project' 'project-327685' was created.
PC PropertyChange 'Name' has been set to 'test006'.
CB CreateBusiness 'project' 'project-327685' was created.
PB PropertyChangeBusiness 'Name' has been updated from 'test006' to 'test007'.

FYI: In the early Cordys-days of the platform an entity was called a “Business Object”!

AND…I found more after some remote debugging and decompiling in an interesting JAR file named entityCore.jar:

Code Instruction
AB AactionBusiness
CC CancelCheckout
CV CurrentVersion
AV AccessViolation
XS (X)InvalidSignature
BS BusinessWorkSpace

This list goes on and on…
How did I find them?…Well, that’s the power of Unix:

1
2
3
4
5
6
find /opt/tomee/latest/webapps/home#app#entityRestService/WEB-INF/lib/ -iname 'entityCore.jar' -print | while read jar; do
echo "$jar:"
unzip -qq -l $jar | sed 's/.* //' | while read cls; do
unzip -c $jar $cls | grep -q 'HistoryEventAttribute' && echo " "$cls
done
done

That’s what I call reverse-engineering! 😎

What about the compression types ‘Deflate’ and ‘GZip’?

Well, deflate (get air out of your camping-bed to make it smaller) is the opposite of inflate (putting air into your camping-bed, making it bigger)! Sounds like zip/unzip; So, what’s the difference with GZip? Only one way to find out!

After some research, decompiling, and remote debugging both use these corresponding standard Java classes (as expected):

1
2
3
4
java.util.zip.Inflater
java.util.zip.Deflater
java.util.zip.GZIPInputStream
java.util.zip.GZIPOutputStream

Notes:

  • You can also play with this stuff via the multiutil website!
  • If you want to play around yourself; Have a look at my GitRepo class ‘TestStreamUtils’
  • When you’re beyond curiosity; Read this

There is only one thing I wasn’t able to accomplish; That’s making a copy of the database value for a ‘Deflate’, and ‘GZip’ option (the values starting with \x...) and reconstruct this to a valid String as input for my test class. Simple and efficient like the convert_from(s_history_data, 'UTF8') function of Postgres. There is some conversion going on from the database value into the Java code. If you have any idea? Comment me below! This is the history data export from my database in case you wonder…


I give it a 99,99% “DONE” with some valuable API calls to get an answer to the initial colleague question how to read the persistent history data after an entity instance in runtime is removed. The answer is found, my colleague was happy with the answer, I could help him…That’s how we evolve overtime! Sharing is caring and from 4 years of sharing this far, I can tell you that it’s the best thing I’ve done in my 20+ career as a consultant. Have a great weekend…I see you next week! Cheers. 🍺

Don’t forget to subscribe to get updates on the activities happening on this site. Have you noticed the quiz where you find out if you are also “The AppWorks guy”?