From eb9e4362f09bf07892f4b593d0bb100d8e7bfb98 Mon Sep 17 00:00:00 2001
From: Malte Kremer <m.kremer@adito.de>
Date: Wed, 2 Jun 2021 11:15:57 +0000
Subject: [PATCH] =?UTF-8?q?1081754/1079943=20Timeshift=20in=20Basic=20?=
 =?UTF-8?q?=C3=BCbernehmen?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 process/DataTimeshift/DataTimeshift.aod       |   9 +
 process/DataTimeshift/process.js              | 135 ++++++++++++++
 .../DataTimeshift_lib/DataTimeshift_lib.aod   |  10 +
 process/DataTimeshift_lib/process.js          | 176 ++++++++++++++++++
 .../DatabaseSupplier_lib.aod                  |   9 +
 process/DatabaseSupplier_lib/process.js       |  77 ++++++++
 process/Supplier_lib/Supplier_lib.aod         |   9 +
 process/Supplier_lib/process.js               |  27 +++
 8 files changed, 452 insertions(+)
 create mode 100644 process/DataTimeshift/DataTimeshift.aod
 create mode 100644 process/DataTimeshift/process.js
 create mode 100644 process/DataTimeshift_lib/DataTimeshift_lib.aod
 create mode 100644 process/DataTimeshift_lib/process.js
 create mode 100644 process/DatabaseSupplier_lib/DatabaseSupplier_lib.aod
 create mode 100644 process/DatabaseSupplier_lib/process.js
 create mode 100644 process/Supplier_lib/Supplier_lib.aod
 create mode 100644 process/Supplier_lib/process.js

diff --git a/process/DataTimeshift/DataTimeshift.aod b/process/DataTimeshift/DataTimeshift.aod
new file mode 100644
index 0000000000..7ed0045dd2
--- /dev/null
+++ b/process/DataTimeshift/DataTimeshift.aod
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<process xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.2.2" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/process/1.2.2">
+  <name>DataTimeshift</name>
+  <majorModelMode>DISTRIBUTED</majorModelMode>
+  <process>%aditoprj%/process/DataTimeshift/process.js</process>
+  <variants>
+    <element>EXECUTABLE</element>
+  </variants>
+</process>
diff --git a/process/DataTimeshift/process.js b/process/DataTimeshift/process.js
new file mode 100644
index 0000000000..c5cc7fd57e
--- /dev/null
+++ b/process/DataTimeshift/process.js
@@ -0,0 +1,135 @@
+import("system.logging");
+import("system.text");
+import("system.vars");
+import("system.eMath");
+import("Employee_lib");
+import("PermissionCalendar_lib");
+import("system.tools");
+import("system.calendars");
+import("Sql_lib");
+import("system.datetime");
+import("system.db");
+import("system.project");
+import("DataTimeshift_lib");
+
+var userTitle = vars.get("$sys.user");
+//only allow the timeshifting-service user to perform this because the process_audit checks the origin of a change
+//=> a no-audit alias is not required
+if (userTitle != TimeShiftUtils.getTimeShiftingServiceUserTitle())
+    throw new Error("[dataTimeshifting_serverProcess]Wrong user started the process. Given user: \"" + userTitle + "\""
+        + " but required user is \"" + TimeShiftUtils.getTimeShiftingServiceUserTitle() + "\"");
+
+//the reference timestamp (=the time when the last time shifting has been performed) is stored in the service users params
+var user = tools.getUser(userTitle, tools.PROFILE_FULL);
+var referenceTimestamp = user[tools.PARAMS]["timeshiftReferenceTimestamp"];
+if (!referenceTimestamp)
+    throw new Error("[dataTimeshifting_serverProcess]The given timeshiftReferenceTimestamp paran is empty."
+        + "Tried to receive the param from user: \"" + userTitle + "\"");
+
+referenceTimestamp = datetime.toLong(referenceTimestamp, "yyyy-MM-dd'T'HH:mm:ssZ", "UTC");
+var now = datetime.date();
+var timeshiftingTime = eMath.subInt(now, referenceTimestamp);
+_getCommonTableShifter().timeshift(timeshiftingTime);
+
+_getBinariesShifter().timeshift(timeshiftingTime);
+
+
+//calendars are different and need a custom treatment, since the calendars.***-methods are used
+var calendarShifter = _getCalendarShifter();
+if (calendarShifter) //is null when calendarbackend != DB
+{
+    calendarShifter.timeshift(timeshiftingTime);
+}
+//Initial reference point is: "2020-09-16T00:00:00+0000";
+user[tools.PARAMS]["timeshiftReferenceTimestamp"] = datetime.toDate(now, "yyyy-MM-dd'T'HH:mm:ssZ", "UTC");
+tools.updateUser(user);
+/**
+ * Creates a TimeShifter object for the "Data_alias"-alias and returns the object.
+ * ASYS_*** tables are skipped as well as the liquibase-tables "DATABASECHANGELOG" and "DATABASECHANGELOGLOCK"
+ */
+function _getCommonTableShifter()
+{
+    var alias = "Data_alias";
+    var timeshifter = new TimeShifter(alias);
+
+    var excludedTables = ["DATABASECHANGELOG",  "DATABASECHANGELOGLOCK"];
+    var tables = db.getTables(alias);
+    var tablesToShift = timeshifter.getTablesToShift();
+    tables.forEach(function (tableName){
+        if (!tableName.startsWith("ASYS_") && !excludedTables.includes(tableName))
+            tablesToShift.add(tableName);
+    });
+    timeshifter.setTablesToShift( tablesToShift);
+    timeshifter.getTablesToShift().forEach(function(table){ str_log+= "\n"+table; });
+    return timeshifter;
+}
+
+/**
+ * Creates a TimeShifter object for the ASYS_BINARIES and returns that object.
+ */
+function _getBinariesShifter()
+{
+    var alias = SqlUtils.getBinariesAlias();
+    var timeshifter = new TimeShifter(alias);
+    timeshifter.getTablesToShift().add("ASYS_BINARIES");
+    return timeshifter;
+}
+
+/**
+ * Creates a TimeShifter object for calendar events and returns it.
+ * This will only work if the calendar-event backend is database and appopriate grants are set.
+ */
+function _getCalendarShifter()
+{
+    var backendType = calendars.getBackendType();
+    if (backendType == calendars.BACKEND_DB)
+    {
+        var alias = project.getInstanceConfigValue("calendarAlias", null);
+        //calendars set grant to everyone here
+        var timeshifter = new TimeShifter(alias);
+        timeshifter.getTablesToShift().add("ASYS_CALENDARBACKEND");
+        timeshifter.getCustomTreatments().set("ASYS_CALENDARBACKEND", function(pTimeDiff){
+            var entryQuery = newSelect("ASYS_CALENDARBACKEND.ELEMENTUID, min(ASYS_CALENDARBACKEND.OWNER)", alias)
+                .from("ASYS_CALENDARBACKEND")
+                .where("ASYS_CALENDARBACKEND.ENTRYTYPE", calendars.VEVENT)
+                .and("ASYS_CALENDARBACKEND.CLASSIFICATION", 0)
+                .and("VCOMPONENT not like '%CATEGORIES:Feiertag%'")
+                .groupBy("ASYS_CALENDARBACKEND.ELEMENTUID");
+
+            entryQuery.pageSize(500);
+            entryQuery.forEachPage(function(pEntries){
+                for (var i = 0, l = pEntries.length; i < l; i++)
+                {
+                    var entry = calendars.getEntry(pEntries[i][0], null, _getTitleCalenderUser(pEntries[i][1]), calendars.VEVENT);
+                    entry[calendars.DTSTART] = String(eMath.addInt(entry[calendars.DTSTART], pTimeDiff));
+                    entry[calendars.DTEND] = String(eMath.addInt(entry[calendars.DTEND], pTimeDiff));
+                    calendars.update([entry]);
+                }
+            });
+        });
+        return timeshifter;
+    }
+    return null;
+}
+
+/**
+ * Parses a calendarUser in calendaruser-form (e.g.: "; mailo: hans.mueller@mailing.de;" and returns the adito login name
+ */
+function _getTitleCalenderUser(pCalendarUser)
+{
+    var data = text.decodeMS(pCalendarUser)
+    for ( var i = 0; i < data.length; i++ )
+    {
+        //if login changes we have to check calendarid
+        if ( data[i].substr(0, "mailto:".length).toUpperCase() == "MAILTO:" )
+        {
+            var user = tools.getUserByAttribute(tools.CALENDARID, [data[i].substr("mailto:".length)]);
+            if (user != null )
+                return user[tools.TITLE];
+        }
+
+        if ( data[i].substr(0, 3).toUpperCase() == "CN:" )
+            return data[i].substr(3);
+    }
+    return "";
+}
diff --git a/process/DataTimeshift_lib/DataTimeshift_lib.aod b/process/DataTimeshift_lib/DataTimeshift_lib.aod
new file mode 100644
index 0000000000..29d178a8be
--- /dev/null
+++ b/process/DataTimeshift_lib/DataTimeshift_lib.aod
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<process xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.2.2" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/process/1.2.2">
+  <name>DataTimeshift_lib</name>
+  <majorModelMode>DISTRIBUTED</majorModelMode>
+  <process>%aditoprj%/process/DataTimeshift_lib/process.js</process>
+  <alias>Data_alias</alias>
+  <variants>
+    <element>LIBRARY</element>
+  </variants>
+</process>
diff --git a/process/DataTimeshift_lib/process.js b/process/DataTimeshift_lib/process.js
new file mode 100644
index 0000000000..1d32d54279
--- /dev/null
+++ b/process/DataTimeshift_lib/process.js
@@ -0,0 +1,176 @@
+import("Util_lib");
+import("Sql_lib");
+import("system.translate");
+import("system.SQLTYPES");
+import("system.logging");
+import("system.tools");
+import("system.calendars");
+import("system.datetime");
+import("system.eMath");
+import("system.db");
+import("system.project");
+import("DatabaseSupplier_lib");
+import("Supplier_lib");
+
+/**
+ * Object for helping to shift DATE/TIMESTAMP-databasefields of an alias.
+ * @param {String} pAlias <p/>database alias for the shifting transactions. Data will be read from this alias and written into this alias
+ * @throws Error when no alias is provided
+ */
+function TimeShifter(pAlias) 
+{
+    if (pAlias === undefined)
+        throw new Error("[TimeShifter.constructor]No alias is defined, but it is required");
+    this._alias = pAlias;
+    this._tablesToUpdate = new Set();
+    this._customTreatments = new Map();
+    
+    this._defaulTreatmentFunc = function(pDifferenceMillis, pTablename, pPrimaryKeySupplier, pColumnsSupplier, pColumnTypeSupplier) {
+        var columnNames = pColumnsSupplier.get();
+        var columnTypes = pColumnTypeSupplier.get();
+        var pkField = pPrimaryKeySupplier.get();
+        var alias = this.getAlias();
+        
+        if (!pkField)//we cannot update if there is no primary key field
+            return;
+        
+        var dateCols = [];
+        var dateColTypes = [];
+        for (var i = 0, l = columnNames.length; i < l; i++)
+        {
+            if (columnTypes[i] == SQLTYPES.TIMESTAMP || columnTypes[i] == SQLTYPES.DATE)
+            {
+                dateCols.push(columnNames[i]);
+                dateColTypes.push(columnTypes[i]);
+            }
+        }
+        if (dateCols.length == 0)
+            return;
+
+        var queryFields = [pkField].concat(dateCols);
+        var dataQuery = newSelect(queryFields, this.getAlias())
+                            .from(pTablename)
+                            //some databases need a sorting for correct paging
+                            .orderBy(pkField);
+        
+        dataQuery.pageSize(500);
+        dataQuery.forEachPage(function (pData){
+            if (pData.length == 0)
+                return;
+            var updateStatements = [];
+            for (var i = 0, l = pData.length; i < l; i++)
+            {
+                var vals = pData[i].slice(1);//first values is always the primary key
+                //reduce the array to an object where the key is the columnname and the value is the target value for the update
+                vals = vals.reduce(function(pUpdateObject, pValueToAdd, pIndex){
+                        //empty timestamp should be ignored
+                        if (pValueToAdd == "")
+                            return pUpdateObject;
+                        var column = dateCols[pIndex];
+                        pUpdateObject[column] = eMath.addInt(pValueToAdd, pDifferenceMillis);
+                        return pUpdateObject;
+                    }, {});
+                //if a field is empty it is not added so it can happen that the object is empty when there are no date fields filled
+                if ( !Utils.isEmpty( vals))
+                {
+                    updateStatements.push(
+                        newWhere([pTablename, pkField], pData[i][0], null, null, alias)
+                            .buildUpdateStatement(vals, pTablename)
+                        );
+                }
+            }
+            if (updateStatements.length > 0)
+                db.updates(updateStatements, alias);
+        },null, null, this);
+    };
+}
+
+/**
+ *
+ * @return {String} returns the alias that is set for the TimeShifter-object
+ */
+TimeShifter.prototype.getAlias = function(){
+
+    return this._alias;
+};
+
+/**
+ *
+ * @return {Set} <p/>returns a Set where the tables that shall be shiftet can be added
+ * 
+ * @example
+ * var exampleShifter = new TimeShifter("example_dbalias");
+ * exampleShifter.getTablesToShift().add("EXAMPLE_TABLE");
+ */
+TimeShifter.prototype.getTablesToShift = function(){
+    return this._tablesToUpdate;
+};
+TimeShifter.prototype.setTablesToShift = function( pTablesToUpdate){
+    this._tablesToUpdate = pTablesToUpdate;
+}
+
+/**
+ *
+ * @return {Map} <p/>returns a Map where the key is the tablename and the value is a callback function that performs the shifting with the following
+ *                   params: 
+ *                   <ul>
+ *                      <li>{String} timeDiff - the difference in milliseconds where the shift has to be performed</li>
+ *                      <li>{String} tableName - name of the table that is shifted</li>
+ *                      <li>{CachedSupplierWrapper} pkSupplier - a cached "PrimaryKeyNameSupplier" to retrieve the primary key name of the table</li>
+ *                      <li>{CachedSupplierWrapper} columnSupplier - a cached "ColumnSupplier" to retrieve all columns of the table</li>
+ *                      <li>{CachedSupplierWrapper} columnTypesSupplier - a cached "ColumnTypesSupplier" to retrieve all columntypes of the table</li>
+ *                   </ul>
+ *                   <p/>You can access the TimeShifter-object via the "this" reference 
+ * 
+ * @example
+ * var exampleShifter = new TimeShifter("example_dbalias");
+ * exampleShifter.getTablesToShift().add("EXAMPLE_TABLE");
+ * exampleShifter.getCustomTreatments().set("EXAMPLE_TABLE", function(pTimeDiff, pTable, pPkSupplier, pColumnSupplier, pColumnTypeSupplier){
+ *     var primaryKeyField = pPkSupplier.get();
+ *     //code for shifting ......
+ * );
+ */
+TimeShifter.prototype.getCustomTreatments = function(){
+    return this._customTreatments;
+};
+
+/**
+ * After configurating the TimeShifter object the data can be shiftet with the timeshift-function.
+ * 
+ * @param {String|Number} pOffsetInMillis <p/>The amount of milliseconds for shifting the data, if this is e.g. one day (86400000) every date 
+ *                                            will be shiftet one day in the future
+ */
+TimeShifter.prototype.timeshift = function (pOffsetInMillis)
+{   
+    var timeDiff = pOffsetInMillis;
+    this.getTablesToShift().forEach(function (tableName) {
+        var pkSupplier = new CachedSupplierWrapper(new PrimaryKeyNameSupplier(this.getAlias(), tableName));
+        var columnSupplier = new CachedSupplierWrapper(new ColumnSupplier(this.getAlias(), tableName));
+        var columnTypesSupplier = new CachedSupplierWrapper(new ColumnTypesSupplier(this.getAlias(), tableName, columnSupplier));
+        if (this.getCustomTreatments().has(tableName))
+        {
+            var customTreamentFunc = this.getCustomTreatments().get(tableName);
+            //assertions
+            customTreamentFunc.call(this, timeDiff, tableName, pkSupplier, columnSupplier, columnTypesSupplier);
+        }
+        else
+        {
+            this._defaulTreatmentFunc.call(this, timeDiff, tableName, pkSupplier, columnSupplier, columnTypesSupplier);
+        }
+    }, this);
+    this.setTablesToShift( new Set())
+};
+
+/**
+ * Provides static utility functions for shifting timestamp database ifelds
+ * Don't instanciate TimeShiftUtils.
+ */
+function TimeShiftUtils(){}
+
+/**
+ *
+ * @return Title of the user that is used for timeshifting-actions
+ */
+TimeShiftUtils.getTimeShiftingServiceUserTitle = function (){
+    return "timeshiftService";
+}
\ No newline at end of file
diff --git a/process/DatabaseSupplier_lib/DatabaseSupplier_lib.aod b/process/DatabaseSupplier_lib/DatabaseSupplier_lib.aod
new file mode 100644
index 0000000000..a9d5d7e631
--- /dev/null
+++ b/process/DatabaseSupplier_lib/DatabaseSupplier_lib.aod
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<process xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.2.2" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/process/1.2.2">
+  <name>DatabaseSupplier_lib</name>
+  <majorModelMode>DISTRIBUTED</majorModelMode>
+  <process>%aditoprj%/process/DatabaseSupplier_lib/process.js</process>
+  <variants>
+    <element>LIBRARY</element>
+  </variants>
+</process>
diff --git a/process/DatabaseSupplier_lib/process.js b/process/DatabaseSupplier_lib/process.js
new file mode 100644
index 0000000000..f9052970b6
--- /dev/null
+++ b/process/DatabaseSupplier_lib/process.js
@@ -0,0 +1,77 @@
+import("system.logging");
+import("system.db");
+import("system.project");
+
+
+/*
+ * Database supplier lib, supplies a unified object structure for loading table definitions
+ */
+function AbstractDbTableBasedSupplier(pAlias, pTable)
+{
+    this._alias = pAlias;
+    this._table = pTable;
+}
+
+AbstractDbTableBasedSupplier.prototype.get = function()
+{
+    throw new Error("[AbstractDbTableBasedSupplier.get]Not implemented because it is abstract.");
+};
+
+
+//Supplier for pks
+function PrimaryKeyNameSupplier(pAlias, pTable)
+{
+    AbstractDbTableBasedSupplier.call(this, pAlias, pTable);
+}
+PrimaryKeyNameSupplier.prototype = Object.create(AbstractDbTableBasedSupplier.prototype);
+PrimaryKeyNameSupplier.prototype.constructor = AbstractDbTableBasedSupplier;
+
+//uses alias definitions to return the primary key name for pTable defined in the constructor
+PrimaryKeyNameSupplier.prototype.get = function()
+{
+    if (!this.hasOwnProperty(this._resultCache))
+    {
+        var struct = project.getAliasDefinitionStructure(this._alias, this._table);
+        var tableStruct = struct.tables[ this._table.toUpperCase()];
+        if (tableStruct == undefined){
+            this._resultCache = "";
+            logging.log( this._table +"'s alias definition could not be found, primary key could not be established");
+        }else
+            this._resultCache = tableStruct.idColumn || "";
+         
+    }
+    return this._resultCache;
+};
+
+//Supplier for column names
+function ColumnSupplier(pAlias, pTable)
+{
+    AbstractDbTableBasedSupplier.call(this, pAlias, pTable);
+}
+ColumnSupplier.prototype = Object.create(AbstractDbTableBasedSupplier.prototype);
+ColumnSupplier.prototype.constructor = AbstractDbTableBasedSupplier;
+
+//returns all column names of a table pTable defined in the constructor
+ColumnSupplier.prototype.get = function()
+{
+    var columns = db.getColumns(this._table, this._alias);
+    return columns;
+};
+
+
+//Supplier for column types
+function ColumnTypesSupplier(pAlias, pTable, pColumnSupplier)
+{
+    AbstractDbTableBasedSupplier.call(this, pAlias, pTable);
+    this._columnSuppier = pColumnSupplier;
+}
+ColumnTypesSupplier.prototype = Object.create(AbstractDbTableBasedSupplier.prototype);
+ColumnTypesSupplier.prototype.constructor = AbstractDbTableBasedSupplier;
+
+//returns column types for all columns of table pTable defined in the constructor
+ColumnTypesSupplier.prototype.get = function()
+{
+    var columns = this._columnSuppier.get();
+    var columnTypes = db.getColumnTypes(this._table, columns, this._alias);
+    return columnTypes;
+};
\ No newline at end of file
diff --git a/process/Supplier_lib/Supplier_lib.aod b/process/Supplier_lib/Supplier_lib.aod
new file mode 100644
index 0000000000..8bfdde7648
--- /dev/null
+++ b/process/Supplier_lib/Supplier_lib.aod
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<process xmlns="http://www.adito.de/2018/ao/Model" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" VERSION="1.2.2" xsi:schemaLocation="http://www.adito.de/2018/ao/Model adito://models/xsd/process/1.2.2">
+  <name>Supplier_lib</name>
+  <majorModelMode>DISTRIBUTED</majorModelMode>
+  <process>%aditoprj%/process/Supplier_lib/process.js</process>
+  <variants>
+    <element>LIBRARY</element>
+  </variants>
+</process>
diff --git a/process/Supplier_lib/process.js b/process/Supplier_lib/process.js
new file mode 100644
index 0000000000..6c4788e913
--- /dev/null
+++ b/process/Supplier_lib/process.js
@@ -0,0 +1,27 @@
+/*
+ * Wrapper object to cache and load suppliers (see DatabaseSupplier_lib) 
+ *
+ */
+
+function CachedSupplierWrapper(pSupplier)
+{
+    //todo: assertion
+    this._supplier = pSupplier;
+    this._valueLoaded = false;
+    this._cache = null;
+}
+
+CachedSupplierWrapper.prototype.get = function()
+{
+    if (!this._valueLoaded)
+    {
+        this._cache = this.getSupplier().get();
+        this._valueLoaded = true;
+    }
+    return this._cache;
+};
+
+CachedSupplierWrapper.prototype.getSupplier = function ()
+{
+    return this._supplier;
+};
\ No newline at end of file
-- 
GitLab