Compare commits

...

9 Commits

Author SHA1 Message Date
odain
9fd25be17c fix test + phpstan feedbacks 2026-04-24 11:55:22 +02:00
odain
016c7d5fc1 asynchronous cleanup 2026-04-24 11:21:15 +02:00
Eric Espie
d61ddadaa4 Add background operation 2026-04-24 11:11:31 +02:00
odain
e8e842dcea deletion plan test coverage 2026-04-24 11:11:31 +02:00
odain
9b90ed3983 WIP 2026-04-24 11:11:29 +02:00
odain
04380802b6 WIP - prepare cron cleanup + first tests 2026-04-24 11:09:29 +02:00
odain
963da15510 report Exception method coming from develop before merge 2026-04-24 11:07:13 +02:00
Eric Espie
ebbdb426fc Add delta 2026-04-24 11:06:27 +02:00
odain
8534b5af7e N°9165 - Allow direct suppression or delayed suppression depending on the amount of data to remove 2026-04-24 11:06:24 +02:00
18 changed files with 928 additions and 22 deletions

View File

@@ -1,5 +1,121 @@
<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.3">
<classes>
<class id="DataFeatureRemovalBackgroundOperation" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>datafeatureremovalbackgroundoperation</db_table>
<style>
<icon>
<fileref ref="icons8-electricity_643cf7fd7a024968679dc0c35a710a03"/>
</icon>
<main_color>#0000ff</main_color>
</style>
<naming>
<attributes/>
</naming>
<reconciliation>
<attributes/>
</reconciliation>
<uniqueness_rules>
<rule id="list_of_classes">
<attributes>
<attribute id="classes"/>
</attributes>
<filter><![CDATA[]]></filter>
<disabled>false</disabled>
<is_blocking>true</is_blocking>
</rule>
</uniqueness_rules>
</properties>
<fields>
<field id="creation_date" xsi:type="AttributeDateTime">
<sql>creation_date</sql>
<default_value/>
<is_null_allowed>false</is_null_allowed>
<dependencies/>
<tracking_level>all</tracking_level>
</field>
<field id="features_code" xsi:type="AttributeText">
<sql>features_code</sql>
<default_value/>
<is_null_allowed>false</is_null_allowed>
<validation_pattern/>
<width/>
<height/>
<dependencies/>
<tracking_level>all</tracking_level>
</field>
<field id="classes" xsi:type="AttributeText">
<sql>classes</sql>
<default_value/>
<is_null_allowed>false</is_null_allowed>
<validation_pattern/>
<width/>
<height/>
<dependencies/>
<tracking_level>all</tracking_level>
</field>
</fields>
<methods/>
<presentation>
<list>
<items>
<item id="creation_date">
<rank>10</rank>
</item>
<item id="features_code">
<rank>20</rank>
</item>
<item id="classes">
<rank>30</rank>
</item>
</items>
</list>
<search>
<items/>
</search>
<details>
<items>
<item id="creation_date">
<rank>10</rank>
</item>
<item id="features_code">
<rank>20</rank>
</item>
<item id="classes">
<rank>30</rank>
</item>
</items>
</details>
</presentation>
<parent>cmdbAbstractObject</parent>
</class>
</classes>
<dictionaries>
<dictionary id="EN US">
<entries>
<entry id="Class:DataFeatureRemovalBackgroundOperation/Name" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation/ComplementaryName" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation" _delta="define"><![CDATA[DataFeatureRemovalBackgroundOperation]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation/Attribute:creation_date" _delta="define"><![CDATA[Creation date]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation/Attribute:creation_date+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation/Attribute:features_code" _delta="define"><![CDATA[Features code]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation/Attribute:features_code+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation/Attribute:classes" _delta="define"><![CDATA[Classes]]></entry>
<entry id="Class:DataFeatureRemovalBackgroundOperation/Attribute:classes+" _delta="define"><![CDATA[]]></entry>
</entries>
</dictionary>
</dictionaries>
<files>
<file id="icons8-electricity_643cf7fd7a024968679dc0c35a710a03" xsi:type="File" _delta="define_if_not_exists">
<name>images/icons/icons8-electricity.svg</name>
<mime_type>image/svg+xml</mime_type>
<data>PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciICB2aWV3Qm94PSIwIDAgNDggNDgiIHdpZHRoPSIyNDBweCIgaGVpZ2h0PSIyNDBweCI+PHBhdGggZmlsbD0iIzM3YzZkMCIgZD0iTTI2LjQ1MTIsMzdsNy43NzY0Ni0xNS41NTI5MWExLDEsMCwwLDAtLjg5NDM1LTEuNDQ3MjJMMjIuOTUxMiwxOS45OTkyNWw2LjMzNi0xMy41NzYzNUExLDEsMCwwLDAsMjguMzgxLDVIMjIuNTk2MzlhMSwxLDAsMCwwLS45MTExMS41ODc4M2wtOC41OTUyNCwxOUExLDEsMCwwLDAsMTQuMDAxMTUsMjZoMTAuNDVsLTUuNSwxMVoiLz48cGF0aCBmaWxsPSIjMzdjNmQwIiBkPSJNMTYuMjIyODYsMzdIMjkuODY0NzFhLjU1NzM1LjU1NzM1LDAsMCwxLC4zNTgxNSwxTDE5Ljk2LDQ0LjM2N2ExLjAzMzY0LDEuMDMzNjQsMCwwLDEtMS40OTEyMS0uNDI1ODZMMTUuNzIyODYsMzcuNzVBLjUwNDUuNTA0NSwwLDAsMSwxNi4yMjI4NiwzN1oiLz48L3N2Zz4=</data>
</file>
</files>
<menus>
<menu id="DataFeatureRemovalMenu" xsi:type="WebPageMenuNode" _delta="define">
<rank>30</rank>

View File

@@ -23,6 +23,7 @@ Dict::Add('EN US', 'English', 'English', [
'DataFeatureRemoval:Features:Title' => 'Features',
'DataFeatureRemoval:Analysis:Title' => 'Analysis result',
'DataFeatureRemoval:Analysis:SubTitle' => '%1$s element(s) to clean before continuing',
'DataFeatureRemoval:Analysis:Ok' => "No data to cleanup",
'DataFeatureRemoval:DeletionPlan:Title' => 'Deletion plan',
'DataFeatureRemoval:DeletionPlan:SubTitle' => '%1$s rows to clean before continuing',
@@ -40,6 +41,7 @@ Dict::Add('EN US', 'English', 'English', [
'UI:Button:AnalyzeAndSetup' => 'Analyze and go to setup',
'UI:Button:PlanDeletion' => 'Prepare deletion plan',
'UI:Button:DoDeletion' => 'Delete data',
'UI:Button:DoAsyncDeletion' => 'Do asynchronous deletion',
'UI:Button:BackToMain' => 'Back to Feature Removal',
'UI:Button:Setup' => 'Back to setup',

View File

@@ -23,6 +23,7 @@ Dict::Add('FR FR', 'French', 'Français', [
'DataFeatureRemoval:Features:Title' => 'Fonctionnalités',
'DataFeatureRemoval:Analysis:Title' => 'Résultat de lanalyse',
'DataFeatureRemoval:Analysis:SubTitle' => '%1$s élément(s) à nettoyer avant de poursuivre',
'DataFeatureRemoval:Analysis:Ok' => "Aucune donnée à nettoyer",
'DataFeatureRemoval:DeletionPlan:Title' => 'Plan de suppression',
'DataFeatureRemoval:DeletionPlan:SubTitle' => '%1$s ligne(s) à nettoyer avant de poursuivre',
@@ -39,6 +40,7 @@ Dict::Add('FR FR', 'French', 'Français', [
'UI:Button:ModifyChoices' => 'Modifier les choix',
'UI:Button:AnalyzeAndSetup' => 'Analyser et ouvrir lassistant de configuration',
'UI:Button:PlanDeletion' => 'Préparer le plan de suppression',
'UI:Button:DoAsyncDeletion' => 'Lancer la suppression en tâche de fond',
'UI:Button:DoDeletion' => 'Supprimer les données',
'UI:Button:BackToMain' => 'Retour à la suppression de fonctionnalités',
'UI:Button:Setup' => 'Retour à lassistant de configuration',

View File

@@ -31,6 +31,7 @@ SetupWebPage::AddModule(
'datamodel' => [
'vendor/autoload.php',
'model.combodo-data-feature-removal.php', // Contains the PHP code generated by the "compilation" of datamodel.combodo-data-feature-removal.xml
'src/Hook/DataFeatureRemovalBackgroundTask.php',
],
'webservice' => [],
'data.struct' => [

View File

@@ -14,6 +14,7 @@ use Combodo\iTop\Application\TwigBase\Controller\Controller;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalConfig;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalHelper;
use Combodo\iTop\DataFeatureRemoval\Service\BackgroundOperationService;
use Combodo\iTop\DataFeatureRemoval\Service\DataFeatureRemoverExtensionService;
use Combodo\iTop\DataFeatureRemoval\Service\DeletionPlanService;
use Combodo\iTop\Setup\FeatureRemoval\DryRemovalRuntimeEnvironment;
@@ -39,15 +40,19 @@ class DataFeatureRemovalController extends Controller
$this->AddAnalyzeParams();
$aParams['sTransactionId'] = utils::GetNewTransactionId();
$aParams['aExtensions'] = $this->GetExtensionsTable();
$aParams['aExtensionsCode'] = $this->aSelectedExtensionsForCheck;
$aParams['aAnalysisDataTable'] = $this->aAnalysisDataTable;
$aParams['aClasses'] = array_keys($this->aCountClassesToCleanup);
$aParams['DataFeatureRemovalErrorMessage'] = $sErrorMessage;
$aParams['bHasData'] = $this->iCount > 0;
$aParams['sSetupUrl'] = utils::GetAbsoluteUrlAppRoot().'setup';
$aParams['iCount'] = $this->iCount;
$aParams['DataFeatureRemovalErrorMessage'] = $sErrorMessage;
$aParams['bAnalysisOk'] = (count($this->aCountClassesToCleanup) > 0) && ($this->iCount === 0);
$this->AddLinkedStylesheet(utils::GetAbsoluteUrlModulesRoot().DataFeatureRemovalHelper::MODULE_NAME.'/assets/css/DataFeatureRemoval.css');
$this->AddLinkedScript(utils::GetAbsoluteUrlModulesRoot().DataFeatureRemovalHelper::MODULE_NAME.'/assets/js/DataFeatureRemoval.js');
$this->m_sOperation = "Main";
$this->DisplayPage($aParams);
}
@@ -122,6 +127,7 @@ class DataFeatureRemovalController extends Controller
$aParams['sTransactionId'] = utils::GetNewTransactionId();
$aParams['aDeletionPlanSummary'] = $this->GetTableData('Extensions', $aColumns, $aRows);
$aParams['aClasses'] = $aClasses;
$aParams['aExtensionsCode'] = utils::ReadPostedParam('aExtensionsCode', []);
$aParams['iQueryCount'] = $iQueryCount;
$aParams['bDeletionPossible'] = ($iQueryCount <= DataFeatureRemovalConfig::GetInstance()->Get('max_count_estimation_for_safe_cleanup', 100));
@@ -134,6 +140,14 @@ class DataFeatureRemovalController extends Controller
$this->ValidateTransactionId();
$aClasses = utils::ReadPostedParam('classes', null, utils::ENUM_SANITIZATION_FILTER_CLASS);
$sDeletionButtonValue = utils::ReadPostedParam('btn_deletion', null);
$bAsynchronous = ('async_deletion' === $sDeletionButtonValue);
if ($bAsynchronous) {
BackgroundOperationService::GetInstance()->CreateOperation(utils::ReadPostedParam('aExtensionsCode', []), $aClasses);
$this->OperationMain();
return;
}
$aDeletionExecutionSummary = DeletionPlanService::GetInstance()->ExecuteDeletionPlan($aClasses);
$aColumns = ['Class', 'DeletedCount' , 'UpdatedCount'];
@@ -164,16 +178,24 @@ class DataFeatureRemovalController extends Controller
foreach (DataFeatureRemoverExtensionService::GetInstance()->ReadItopExtensions() as $sCode => $oExtension) {
/** @var \iTopExtension $oExtension */
$bCleanupOngoing = BackgroundOperationService::GetInstance()->IsExtensionBeingCleaned($sCode);
$sChecked = '';
$sDisabledHtml = '';
if ($oExtension->bRemovedFromDisk) {
$sLabel = $oExtension->sLabel;
if ($bCleanupOngoing) {
$sDisabledHtml = 'disabled=""';
$sLabel .= <<<HTML
&nbsp; <span class="ibo-spinner ibo-is-inline ibo-spinner ibo-block" data-role="ibo-spinner"><i class="ibo-spinner--icon fas fa-sync-alt fa-spin" aria-hidden="true"/></span>
HTML;
;
} elseif ($oExtension->bRemovedFromDisk) {
$sDisabledHtml = 'disabled=""';
$sChecked = 'checked';
} elseif (in_array($sCode, $this->aSelectedExtensionsForCheck)) {
$sChecked = 'checked';
}
$sLabel = $oExtension->sLabel;
$sVersion = $oExtension->sVersion;
$sIdEnable = "aExtensions[$sCode][enable]";
@@ -231,12 +253,12 @@ HTML,
}
/**
* @return void
* @return array
*/
public function ReadRemovedExtensions(): void
public function ReadRemovedExtensions(): array
{
if (count($this->aSelectedExtensionsForCheck) > 0) {
return;
return $this->aSelectedExtensionsForCheck;
}
$aSelectedExtensionsFromUI = utils::ReadPostedParam('aExtensions', []);
@@ -253,5 +275,7 @@ HTML,
$this->aSelectedExtensionsForCheck[] = $sCode;
}
}
return $this->aSelectedExtensionsForCheck;
}
}

View File

@@ -7,6 +7,7 @@
namespace Combodo\iTop\DataFeatureRemoval\Helper;
use Config;
use MetaModel;
use utils;
@@ -49,4 +50,22 @@ class DataFeatureRemovalConfig
$oConfig = utils::GetConfig();
$oConfig->SetModuleSetting(DataFeatureRemovalHelper::MODULE_NAME, $sParamName, $value);
}
/**
* @param \Config|null $oConfig
*
* @return void
* @throws \ConfigException
* @throws \CoreException
*/
public function SaveItopConfiguration(Config $oConfig = null)
{
if (is_null($oConfig)) {
$oConfig = utils::GetConfig();
}
$sConfigFile = APPROOT.'conf/'.utils::GetCurrentEnvironment().'/config-itop.php';
@chmod($sConfigFile, 0770); // Allow overwriting the file
$oConfig->WriteToFile($sConfigFile);
@chmod($sConfigFile, 0444); // Read-only
}
}

View File

@@ -10,4 +10,14 @@ namespace Combodo\iTop\DataFeatureRemoval\Helper;
class DataFeatureRemovalHelper
{
public const MODULE_NAME = 'combodo-data-feature-removal';
public static function IsTimeLimitExceeded(int $iUnixTimeLimit): bool
{
if ($iUnixTimeLimit === 0) {
//no time limit
return false;
}
return (time() > $iUnixTimeLimit);
}
}

View File

@@ -0,0 +1,48 @@
<?php
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalConfig;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalHelper;
use Combodo\iTop\DataFeatureRemoval\Service\BackgroundOperationService;
use Combodo\iTop\DataFeatureRemoval\Service\DeletionPlanService;
class DataFeatureRemovalBackgroundTask implements iBackgroundProcess
{
/**
* @inheritDoc
*/
public function GetPeriodicity()
{
return DataFeatureRemovalConfig::GetInstance()->Get('cron_periodicity_in_s', 10);
}
/**
* @inheritDoc
*/
public function Process($iUnixTimeLimit)
{
$iCount = 0;
while ($oBackgroundOperation = BackgroundOperationService::GetInstance()->GetNext()) {
$aClasses = BackgroundOperationService::GetInstance()->GetClasses($oBackgroundOperation);
$aRes = DeletionPlanService::GetInstance()->ExecuteDeletionPlan($aClasses, $iUnixTimeLimit);
IssueLog::Info(__METHOD__, null, $aRes);
IssueLog::Debug(__METHOD__, null, [
'$iUnixTimeLimit' => $iUnixTimeLimit,
'time' => time(),
'timeout reached' => DataFeatureRemovalHelper::IsTimeLimitExceeded($iUnixTimeLimit),
]);
if (DataFeatureRemovalHelper::IsTimeLimitExceeded($iUnixTimeLimit)) {
//timeout reached
return "Handled $iCount operations.";
}
//execution finished before timeout: nothing left to remove
$oBackgroundOperation->DBDelete();
$iCount++;
}
return "Handled $iCount operations.";
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace Combodo\iTop\DataFeatureRemoval\Service;
use DataFeatureRemovalBackgroundOperation;
use DBObjectSet;
use DBSearch;
use MetaModel;
class BackgroundOperationService
{
private static BackgroundOperationService $oInstance;
protected function __construct()
{
}
final public static function GetInstance(): BackgroundOperationService
{
if (!isset(self::$oInstance)) {
self::$oInstance = new BackgroundOperationService();
}
return self::$oInstance;
}
final public static function SetInstance(?BackgroundOperationService $oInstance): void
{
static::$oInstance = $oInstance;
}
/**
* @return \DataFeatureRemovalBackgroundOperation
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MySQLException
* @throws \OQLException
*/
public function GetNext(): ?DataFeatureRemovalBackgroundOperation
{
$sOQL = <<<OQL
SELECT DataFeatureRemovalBackgroundOperation
OQL;
$oSet = new DBObjectSet(DBSearch::FromOQL($sOQL), ['creation_date' => true]);
/** @var DataFeatureRemovalBackgroundOperation $oDBObject */
$oDBObject = $oSet->Fetch();
return $oDBObject;
}
/**
* @param array $aExtensionsCodes
* @param array $aClasses
* @return void
* @throws \ArchivedObjectException
* @throws \CoreCannotSaveObjectException
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \CoreWarning
* @throws \MySQLException
* @throws \OQLException
*/
public function CreateOperation(array $aExtensionsCodes, array $aClasses): void
{
sort($aExtensionsCodes);
sort($aClasses);
$aValues = [
'creation_date' => time(),
'features_code' => '|'.implode('|', $aExtensionsCodes).'|',
'classes' => implode(',', $aClasses),
];
$oObj = MetaModel::NewObject('DataFeatureRemovalBackgroundOperation', $aValues);
$oObj->DBWrite();
}
/**
* @param string $sExtensionCode
* @return bool
* @throws \CoreException
* @throws \MissingQueryArgument
* @throws \MySQLException
* @throws \MySQLHasGoneAwayException
* @throws \OQLException
*/
public function IsExtensionBeingCleaned(string $sExtensionCode): bool
{
$sOQL = <<<OQL
SELECT DataFeatureRemovalBackgroundOperation WHERE features_code LIKE '%|$sExtensionCode|%'
OQL;
$oSet = new DBObjectSet(DBSearch::FromOQL($sOQL));
return $oSet->Count() > 0;
}
/**
* @param \DataFeatureRemovalBackgroundOperation $oBackgroundOperation
* @return array
*/
public function GetClasses(DataFeatureRemovalBackgroundOperation $oBackgroundOperation): array
{
return explode(',', $oBackgroundOperation->Get('classes'));
}
}

View File

@@ -5,6 +5,7 @@ namespace Combodo\iTop\DataFeatureRemoval\Service;
use CMDBSource;
use Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanSummaryEntity;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalHelper;
use DBObjectSearch;
use DeletionPlan;
use MetaModel;
@@ -13,6 +14,8 @@ class DeletionPlanService
{
private static DeletionPlanService $oInstance;
public int $iExecutionCount = 0;
protected function __construct()
{
}
@@ -89,7 +92,10 @@ class DeletionPlanService
}
/**
* @since 3.3.0
* @param array $aClasses
* @param int $iUnixTimeLimit : max execution time in seconds since Epoch before stopping deletion. by default: no limit (ie remove all without stop)
* @param int $iMaxExecutionCount : max execution count before stopping deletion. by default: no limit (ie remove all without stop)
*
* @return array<\Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanSummaryEntity>
* @throws \Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException
@@ -97,7 +103,7 @@ class DeletionPlanService
* @throws \CoreUnexpectedValue
* @throws \MySQLException
*/
public function ExecuteDeletionPlan(array $aClasses): array
public function ExecuteDeletionPlan(array $aClasses, int $iUnixTimeLimit = 0, int $iMaxExecutionCount = -1): array
{
$oDeletionPlan = $this->GetDeletionPlan($aClasses);
@@ -105,11 +111,19 @@ class DeletionPlanService
throw new DataFeatureRemovalException("Deletion Plan cannot be executed due to issues");
}
$this->iExecutionCount = 0;
$aSummary = [];
foreach ($oDeletionPlan->ListUpdates() as $sClass => $aToUpdate) {
$oDeletionPlanSummaryEntity = $aSummary[$sClass] ?? new DeletionPlanSummaryEntity($sClass);
foreach ($aToUpdate as $aData) {
if ($this->IsTimeLimitExceeded($iUnixTimeLimit, $iMaxExecutionCount)) {
$aSummary[$sClass] = $oDeletionPlanSummaryEntity;
return $aSummary;
}
$this->iExecutionCount++;
$oToUpdate = $aData['to_reset'];
/** @var \DBObject $oToUpdate */
foreach ($aData['attributes'] as $sRemoteExtKey => $aRemoteAttDef) {
@@ -126,6 +140,13 @@ class DeletionPlanService
$oDeletionPlanSummaryEntity = $aSummary[$sClass] ?? new DeletionPlanSummaryEntity($sClass);
foreach ($aDeletes as $sId => $aDelete) {
if ($this->IsTimeLimitExceeded($iUnixTimeLimit, $iMaxExecutionCount)) {
$aSummary[$sClass] = $oDeletionPlanSummaryEntity;
return $aSummary;
}
$this->iExecutionCount++;
try {
CMDBSource::Query('START TRANSACTION');
// Delete any existing change tracking about the current object
@@ -144,7 +165,7 @@ class DeletionPlanService
CMDBSource::Query('COMMIT');
} catch (\Exception $e) {
\IssueLog::Exception(__METHOD__.': Cleanup failed', $e);
\IssueLog::Exception(__METHOD__.": Cleanup failed", $e);
CMDBSource::Query('ROLLBACK');
throw $e;
}
@@ -179,4 +200,13 @@ class DeletionPlanService
return $oDeletionPlan;
}
public function IsTimeLimitExceeded(int $iUnixTimeLimit, int $iMaxExecutionCount = -1): bool
{
if (($iMaxExecutionCount !== -1) && ($iMaxExecutionCount <= $this->iExecutionCount)) {
return true;
}
return DataFeatureRemovalHelper::IsTimeLimitExceeded($iUnixTimeLimit);
}
}

View File

@@ -12,9 +12,15 @@
{% for sKey, sClass in aClasses %}
{% UIInput ForHidden { sName:"classes[" ~ sKey ~ "]", sValue:sClass } %}
{% endfor %}
{% for sKey, sCode in aExtensionsCode %}
{% UIInput ForHidden { sName:"aExtensionsCode[" ~ sKey ~ "]", sValue:sCode } %}
{% endfor %}
{% UIToolbar ForButton {} %}
{% UIButton ForPrimaryAction {sLabel:'UI:Button:DoDeletion'|dict_s, sName:'btn_deletion', sId:'btn_deletion', bIsSubmit:true} %}
{% EndUIToolbar %}
{% UIButton ForPrimaryAction {sLabel:'UI:Button:DoAsyncDeletion'|dict_s, sName:'btn_deletion', sId:'btn_async_deletion', sValue: 'async_deletion', bIsSubmit:true} %}
{% EndUIToolbar %}
{% EndUIForm %}
{% else %}
{{ 'DataFeatureRemoval:DeletionPlan:ToManyOperations'|dict_s }}

View File

@@ -12,8 +12,13 @@
{% for sKey, sClass in aClasses %}
{% UIInput ForHidden { sName:"classes[" ~ sKey ~ "]", sValue:sClass } %}
{% endfor %}
{% for sKey, sCode in aExtensionsCode %}
{% UIInput ForHidden { sName:"aExtensionsCode[" ~ sKey ~ "]", sValue:sCode } %}
{% endfor %}
{% UIToolbar ForButton {} %}
{% UIButton ForPrimaryAction {sLabel:'UI:Button:PlanDeletion'|dict_s, sName:'btn_plandeletion', sId:'btn_plandeletion', bIsSubmit:true} %}
{% EndUIToolbar %}
{% EndUIForm %}
{% endif %}

View File

@@ -25,7 +25,9 @@
{% include 'Features.html.twig' %}
{% include 'ExtensionRemovalData.html.twig' %}
{% if not bHasData %}
{% if bAnalysisOk %}
{{ "DataFeatureRemoval:Analysis:Ok"|dict_s }}
{% UIToolbar ForButton {} %}
<a href="{{ sSetupUrl }}">
{% UIButton ForPrimaryAction {sLabel:'UI:Button:Setup'|dict_s, sName:'btn_setup', sId:'btn_setup', bIsSubmit:false} %}

View File

@@ -14,7 +14,10 @@ if (PHP_VERSION_ID < 50600) {
echo $err;
}
}
throw new RuntimeException($err);
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';

View File

@@ -12,6 +12,7 @@ return array(
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalException' => $baseDir . '/src/Helper/DataFeatureRemovalException.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalHelper' => $baseDir . '/src/Helper/DataFeatureRemovalHelper.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalLog' => $baseDir . '/src/Helper/DataFeatureRemovalLog.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\BackgroundOperationService' => $baseDir . '/src/Service/BackgroundOperationService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\DataFeatureRemoverExtensionService' => $baseDir . '/src/Service/DataFeatureRemoverExtensionService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\DeletionPlanService' => $baseDir . '/src/Service/DeletionPlanService.php',
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',

View File

@@ -31,6 +31,7 @@ class ComposerStaticInit4f96a7199e2c0d90e547333758b26464
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalException' => __DIR__ . '/../..' . '/src/Helper/DataFeatureRemovalException.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalHelper' => __DIR__ . '/../..' . '/src/Helper/DataFeatureRemovalHelper.php',
'Combodo\\iTop\\DataFeatureRemoval\\Helper\\DataFeatureRemovalLog' => __DIR__ . '/../..' . '/src/Helper/DataFeatureRemovalLog.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\BackgroundOperationService' => __DIR__ . '/../..' . '/src/Service/BackgroundOperationService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\DataFeatureRemoverExtensionService' => __DIR__ . '/../..' . '/src/Service/DataFeatureRemoverExtensionService.php',
'Combodo\\iTop\\DataFeatureRemoval\\Service\\DeletionPlanService' => __DIR__ . '/../..' . '/src/Service/DeletionPlanService.php',
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',

View File

@@ -10,11 +10,11 @@ namespace Combodo\iTop\Test\UnitTest\Module\DataFeatureRemoval\Service;
use Combodo\iTop\DataFeatureRemoval\Entity\DeletionPlanSummaryEntity;
use Combodo\iTop\DataFeatureRemoval\Helper\DataFeatureRemovalException;
use Combodo\iTop\DataFeatureRemoval\Service\DeletionPlanService;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use Combodo\iTop\Test\UnitTest\ItopCustomDatamodelTestCase;
use DeletionPlan;
/**
* Unit tests for the DeletionPlanService class in the Combodo Data Feature Removal module.
* Unit tests for the DeletionPlanService cf Combodo Data Feature Removal module.
*
* These tests cover:
* - GetDeletionPlanSummary method: handling null and empty input, and verifying summary output for various delete/update scenarios.
@@ -32,14 +32,8 @@ use DeletionPlan;
* @see DeletionPlanSummaryEntity
* @see ItopDataTestCase
*/
class DeletionPlanServiceTest extends ItopDataTestCase
class DeletionPlanServiceTest extends ItopCustomDatamodelTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->RequireOnceItopFile('env-production/combodo-data-feature-removal/vendor/autoload.php');
}
//--- GetDeletionPlanSummary tests ---
/**
@@ -255,8 +249,6 @@ class DeletionPlanServiceTest extends ItopDataTestCase
*/
public function testExecuteDeletionPlanThrowsExceptionWhenIssuesExist(): void
{
$this->RequireOnceItopFile('env-production/combodo-data-feature-removal/src/Helper/DataFeatureRemovalException.php');
$oDeletionPlan = $this->createMock(DeletionPlan::class);
$oDeletionPlan->method('GetIssues')->willReturn(['Some issue']);
$oDeletionPlan->method('ListDeletes')->willReturn([]);
@@ -274,4 +266,197 @@ class DeletionPlanServiceTest extends ItopDataTestCase
$oMockService->ExecuteDeletionPlan(['SomeClass']);
}
public function testExecuteDeletionPlan_DeleteAllWithoutLimit()
{
$this->GivenDFRTreeInDB(<<<EOF
DFRToRemoveLeaf_1 <- DFRToUpdate_1
DFRToRemoveLeaf_1 <- DFRRemovedCollateral_1
DFRRemovedCollateral_1 <- DFRRemovedCollateralCascade_1
EOF);
$aClasses = [ 'DFRToRemoveLeaf' ];
$aRes = DeletionPlanService::GetInstance()->ExecuteDeletionPlan($aClasses);
$aExpected = [
['DFRToUpdate', 1, 0 ],
['DFRToRemoveLeaf', 0, 1 ],
['DFRRemovedCollateral', 0, 1 ],
['DFRRemovedCollateralCascade', 0, 1 ],
];
$this->AssertSummaryEquals($aExpected, $aRes);
}
public function testExecuteDeletionPlan_DeleteManyObjPerClassWithoutLimit()
{
$this->GivenDFRTreeInDB(<<<EOF
DFRToRemoveLeaf_1 <- DFRToUpdate_1
DFRToRemoveLeaf_1 <- DFRRemovedCollateral_1
DFRRemovedCollateral_1 <- DFRRemovedCollateralCascade_1
DFRToRemoveLeaf_2 <- DFRToUpdate_2
DFRToRemoveLeaf_2 <- DFRRemovedCollateral_2
DFRRemovedCollateral_2 <- DFRRemovedCollateralCascade_2
DFRToRemoveLeaf_3 <- DFRToUpdate_3
DFRToRemoveLeaf_3 <- DFRRemovedCollateral_3
DFRRemovedCollateral_3 <- DFRRemovedCollateralCascade_3
EOF);
$aClasses = [ 'DFRToRemoveLeaf' ];
$aRes = DeletionPlanService::GetInstance()->ExecuteDeletionPlan($aClasses);
$aExpected = [
['DFRToUpdate', 3, 0 ],
['DFRToRemoveLeaf', 0, 3 ],
['DFRRemovedCollateral', 0, 3 ],
['DFRRemovedCollateralCascade', 0, 3 ],
];
$this->AssertSummaryEquals($aExpected, $aRes);
}
public function testExecuteDeletionPlan_ManualDeleteShouldFail()
{
$this->GivenDFRTreeInDB(<<<EOF
DFRToRemoveLeaf_1 <- DFRManual_1
EOF);
$aClasses = [ 'DFRToRemoveLeaf' ];
$this->expectException(DataFeatureRemovalException::class);
$this->expectExceptionMessage('Deletion Plan cannot be executed due to issues');
DeletionPlanService::GetInstance()->ExecuteDeletionPlan($aClasses);
}
private function AssertSummaryEquals(array $expected, $actual, $sMessage = '')
{
$aExpected = [];
foreach ($expected as $line) {
$sClass = $line[0];
$iUpdate = $line[1];
$iDelete = $line[2];
$oDeletionPlanSummaryEntity = new DeletionPlanSummaryEntity($sClass);
$oDeletionPlanSummaryEntity->iUpdateCount = $iUpdate;
$oDeletionPlanSummaryEntity->iDeleteCount = $iDelete;
$aExpected[$sClass] = $oDeletionPlanSummaryEntity;
}
$this->assertEquals($aExpected, $actual, $sMessage);
}
public function testExecuteDeletionPlan_StopInUpdates()
{
$this->GivenDFRTreeInDB(<<<EOF
DFRToRemoveLeaf_1 <- DFRToUpdate_1
DFRToRemoveLeaf_1 <- DFRRemovedCollateral_1
DFRRemovedCollateral_1 <- DFRRemovedCollateralCascade_1
DFRToRemoveLeaf_2 <- DFRToUpdate_2
DFRToRemoveLeaf_2 <- DFRRemovedCollateral_2
DFRRemovedCollateral_2 <- DFRRemovedCollateralCascade_2
DFRToRemoveLeaf_3 <- DFRToUpdate_3
DFRToRemoveLeaf_3 <- DFRRemovedCollateral_3
DFRRemovedCollateral_3 <- DFRRemovedCollateralCascade_3
EOF);
$aClasses = [ 'DFRToRemoveLeaf' ];
$aRes = DeletionPlanService::GetInstance()->ExecuteDeletionPlan($aClasses, 0, 3);
$aExpected = [
['DFRToUpdate', 3, 0 ],
['DFRToRemoveLeaf', 0, 0 ],
];
$this->AssertSummaryEquals($aExpected, $aRes);
}
public function testExecuteDeletionPlan_StopInDeletes()
{
$this->GivenDFRTreeInDB(<<<EOF
DFRToRemoveLeaf_1 <- DFRToUpdate_1
DFRToRemoveLeaf_1 <- DFRRemovedCollateral_1
DFRRemovedCollateral_1 <- DFRRemovedCollateralCascade_1
DFRToRemoveLeaf_2 <- DFRToUpdate_2
DFRToRemoveLeaf_2 <- DFRRemovedCollateral_2
DFRRemovedCollateral_2 <- DFRRemovedCollateralCascade_2
DFRToRemoveLeaf_3 <- DFRToUpdate_3
DFRToRemoveLeaf_3 <- DFRRemovedCollateral_3
DFRRemovedCollateral_3 <- DFRRemovedCollateralCascade_3
EOF);
$aClasses = [ 'DFRToRemoveLeaf' ];
$aRes = DeletionPlanService::GetInstance()->ExecuteDeletionPlan($aClasses, 0, 8);
$aExpected = [
['DFRToUpdate', 3, 0 ],
['DFRToRemoveLeaf', 0, 3 ],
['DFRRemovedCollateral', 0, 2 ],
];
$this->AssertSummaryEquals($aExpected, $aRes);
}
public function testExecuteDeletionPlan_WrongOrderDeletion()
{
$this->GivenDFRTreeInDB(<<<EOF
DFRToRemoveLeaf_1 <- DFRRemovedCollateral_1
DFRRemovedCollateral_1 <- DFRRemovedCollateralCascade_1
DFRToRemoveLeaf_2 <- DFRRemovedCollateral_2
DFRRemovedCollateral_2 <- DFRRemovedCollateralCascade_2
DFRToRemoveLeaf_3 <- DFRRemovedCollateral_3
DFRRemovedCollateral_3 <- DFRRemovedCollateralCascade_3
EOF);
$aClasses = [ 'DFRToRemoveLeaf' ];
$oSet = new \DBObjectSet(\DBObjectSearch::FromOQL("SELECT DFRRemovedCollateral WHERE name='DFRRemovedCollateral_3'"));
$oExpectedObj = $oSet->Fetch();
self::assertNotNull($oExpectedObj);
$aRes = DeletionPlanService::GetInstance()->ExecuteDeletionPlan($aClasses, 0, 5);
$aExpected = [
['DFRToRemoveLeaf', 0, 3 ],
['DFRRemovedCollateral', 0, 2 ],
];
$this->AssertSummaryEquals($aExpected, $aRes);
$oSet = new \DBObjectSet(\DBObjectSearch::FromOQL("SELECT DFRRemovedCollateral WHERE name='DFRRemovedCollateral_3'"));
$oActualObj = $oSet->Fetch();
self::assertNotNull($oActualObj, "Deletion plan executed in wrong order: DFRRemovedCollateralCascade/DFRRemovedCollateral are not valid anymore");
self::assertEquals($oExpectedObj->GetKey(), $oActualObj->GetKey());
}
public function GetDatamodelDeltaAbsPath(): string
{
return __DIR__.'/deletionplan_delta.xml';
}
private function GivenDFRTreeInDB(string $sTree)
{
$aTree = explode("\n", $sTree);
foreach ($aTree as $sLine) {
if (trim($sLine) === "") {
continue;
}
$this->GivenDFRTreeLineInDB($sLine);
}
}
private array $aIdByObjectName = [];
private function GivenDFRTreeLineInDB(string $sLine)
{
list($sLeft, $sRight) = explode('<-', $sLine);
$sLeft = trim($sLeft);
$iLeftId = $this->aIdByObjectName[$sLeft] ?? 0;
if ($iLeftId === 0) {
list($sChildClass, ) = explode('_', $sLeft, 2);
$iLeftId = $this->GivenObjectInDB($sChildClass, ['name' => $sLeft]);
$this->aIdByObjectName[$sLeft] = $iLeftId;
}
$sRight = trim($sRight);
list($sChildClass, ) = explode('_', $sRight, 2);
$iRightId = $this->GivenObjectInDB($sChildClass, ['name' => $sRight, 'extkey_id' => $iLeftId]);
$this->aIdByObjectName[$sRight] = $iRightId;
}
}

View File

@@ -0,0 +1,346 @@
<?xml version="1.0" encoding="UTF-8"?>
<itop_design xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.2">
<classes>
<class id="DFRToRemove" _created_in="itop-structure" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>true</abstract>
<db_table>dfrtoremove</db_table>
<naming>
<attributes/>
</naming>
<reconciliation>
<attributes/>
</reconciliation>
</properties>
<fields>
<field id="name" xsi:type="AttributeString">
<sql>name</sql>
<default_value/>
<is_null_allowed>true</is_null_allowed>
<validation_pattern/>
<dependencies/>
<tracking_level>all</tracking_level>
</field>
</fields>
<methods/>
<presentation>
<list>
<items/>
</list>
<search>
<items/>
</search>
<details>
<items>
<item id="name">
<rank>10</rank>
</item>
</items>
</details>
</presentation>
<parent>cmdbAbstractObject</parent>
</class>
<class id="DFRToUpdate" _created_in="itop-structure" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>dfrtoupdate</db_table>
<naming>
<attributes/>
</naming>
<reconciliation>
<attributes/>
</reconciliation>
</properties>
<fields>
<field id="name" xsi:type="AttributeString">
<sql>name</sql>
<default_value/>
<is_null_allowed>true</is_null_allowed>
<validation_pattern/>
<dependencies/>
<tracking_level>all</tracking_level>
</field>
<field id="extkey_id" xsi:type="AttributeExternalKey">
<sql>extkey_id</sql>
<filter/>
<dependencies/>
<is_null_allowed>true</is_null_allowed>
<target_class>DFRToRemove</target_class>
<on_target_delete>DEL_AUTO</on_target_delete>
<tracking_level>all</tracking_level>
</field>
</fields>
<methods/>
<presentation>
<list>
<items/>
</list>
<search>
<items/>
</search>
<details>
<items>
<item id="name">
<rank>10</rank>
</item>
<item id="extkey_id">
<rank>20</rank>
</item>
</items>
</details>
</presentation>
<parent>cmdbAbstractObject</parent>
</class>
<class id="DFRRemovedCollateral" _created_in="itop-structure" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>dfrremovedcollateral</db_table>
<naming>
<attributes/>
</naming>
<reconciliation>
<attributes/>
</reconciliation>
</properties>
<fields>
<field id="name" xsi:type="AttributeString">
<sql>name</sql>
<default_value/>
<is_null_allowed>true</is_null_allowed>
<validation_pattern/>
<dependencies/>
<tracking_level>all</tracking_level>
</field>
<field id="extkey_id" xsi:type="AttributeExternalKey">
<sql>extkey_id</sql>
<filter/>
<dependencies/>
<is_null_allowed>false</is_null_allowed>
<target_class>DFRToRemove</target_class>
<on_target_delete>DEL_AUTO</on_target_delete>
<tracking_level>all</tracking_level>
</field>
</fields>
<methods/>
<presentation>
<list>
<items/>
</list>
<search>
<items/>
</search>
<details>
<items>
<item id="name">
<rank>10</rank>
</item>
<item id="extkey_id">
<rank>20</rank>
</item>
</items>
</details>
</presentation>
<parent>cmdbAbstractObject</parent>
</class>
<class id="DFRToRemoveLeaf" _created_in="itop-structure" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>dfrtoremoveleaf</db_table>
<naming>
<attributes/>
</naming>
<reconciliation>
<attributes/>
</reconciliation>
</properties>
<fields>
<field id="desc" xsi:type="AttributeString">
<sql>desc</sql>
<default_value/>
<is_null_allowed>true</is_null_allowed>
<validation_pattern/>
<dependencies/>
<tracking_level>all</tracking_level>
</field>
</fields>
<methods/>
<presentation>
<list>
<items/>
</list>
<search>
<items/>
</search>
<details>
<items>
<item id="name">
<rank>10</rank>
</item>
<item id="desc">
<rank>20</rank>
</item>
</items>
</details>
</presentation>
<parent>DFRToRemove</parent>
</class>
<class id="DFRRemovedCollateralCascade" _created_in="itop-structure" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>dfrremovedcollateralcascade</db_table>
<naming>
<attributes/>
</naming>
<reconciliation>
<attributes/>
</reconciliation>
</properties>
<fields>
<field id="name" xsi:type="AttributeString">
<sql>name</sql>
<default_value/>
<is_null_allowed>true</is_null_allowed>
<validation_pattern/>
<dependencies/>
<tracking_level>all</tracking_level>
</field>
<field id="extkey_id" xsi:type="AttributeExternalKey">
<sql>extkey_id</sql>
<filter/>
<dependencies/>
<is_null_allowed>false</is_null_allowed>
<target_class>DFRRemovedCollateral</target_class>
<on_target_delete>DEL_AUTO</on_target_delete>
<tracking_level>all</tracking_level>
</field>
</fields>
<methods/>
<presentation>
<list>
<items/>
</list>
<search>
<items/>
</search>
<details>
<items>
<item id="name">
<rank>10</rank>
</item>
<item id="extkey_id">
<rank>20</rank>
</item>
</items>
</details>
</presentation>
<parent>cmdbAbstractObject</parent>
</class>
<class id="DFRManual" _created_in="itop-structure" _delta="define">
<properties>
<category>bizmodel,searchable</category>
<abstract>false</abstract>
<db_table>dfrmanual</db_table>
<naming>
<attributes/>
</naming>
<reconciliation>
<attributes/>
</reconciliation>
</properties>
<fields>
<field id="name" xsi:type="AttributeString">
<sql>name</sql>
<default_value/>
<is_null_allowed>true</is_null_allowed>
<validation_pattern/>
<dependencies/>
<tracking_level>all</tracking_level>
</field>
<field id="extkey_id" xsi:type="AttributeExternalKey">
<sql>extkey_id</sql>
<filter/>
<dependencies/>
<is_null_allowed>false</is_null_allowed>
<target_class>DFRToRemove</target_class>
<on_target_delete>DEL_MANUAL</on_target_delete>
<tracking_level>all</tracking_level>
</field>
</fields>
<methods/>
<presentation>
<list>
<items/>
</list>
<search>
<items/>
</search>
<details>
<items>
<item id="name">
<rank>10</rank>
</item>
<item id="extkey_id">
<rank>20</rank>
</item>
</items>
</details>
</presentation>
<parent>cmdbAbstractObject</parent>
</class>
</classes>
<dictionaries>
<dictionary id="EN US">
<entries>
<entry id="Class:DFRToRemove/Name" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRToRemove/ComplementaryName" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRToRemove" _delta="define"><![CDATA[DFRToRemove]]></entry>
<entry id="Class:DFRToRemove+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRToRemove/Attribute:name" _delta="define"><![CDATA[Name]]></entry>
<entry id="Class:DFRToRemove/Attribute:name+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRToUpdate/Name" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRToUpdate/ComplementaryName" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRToUpdate" _delta="define"><![CDATA[DFRToUpdate]]></entry>
<entry id="Class:DFRToUpdate+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRToUpdate/Attribute:name" _delta="define"><![CDATA[Name]]></entry>
<entry id="Class:DFRToUpdate/Attribute:name+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRToUpdate/Attribute:extkey_id" _delta="define"><![CDATA[Dfrtoremove id]]></entry>
<entry id="Class:DFRToUpdate/Attribute:extkey_id+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRRemovedCollateral/Name" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRRemovedCollateral/ComplementaryName" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRRemovedCollateral" _delta="define"><![CDATA[DFRRemovedCollateral]]></entry>
<entry id="Class:DFRRemovedCollateral+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRRemovedCollateral/Attribute:name" _delta="define"><![CDATA[Name]]></entry>
<entry id="Class:DFRRemovedCollateral/Attribute:name+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRRemovedCollateral/Attribute:extkey_id" _delta="define"><![CDATA[Dfrtoremove id]]></entry>
<entry id="Class:DFRRemovedCollateral/Attribute:extkey_id+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRToRemoveLeaf/Name" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRToRemoveLeaf/ComplementaryName" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRToRemoveLeaf" _delta="define"><![CDATA[DFRToRemoveLeaf]]></entry>
<entry id="Class:DFRToRemoveLeaf+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRToRemoveLeaf/Attribute:desc" _delta="define"><![CDATA[Desc]]></entry>
<entry id="Class:DFRToRemoveLeaf/Attribute:desc+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRRemovedCollateralCascade/Name" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRRemovedCollateralCascade/ComplementaryName" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRRemovedCollateralCascade" _delta="define"><![CDATA[DFRRemovedCollateralCascade]]></entry>
<entry id="Class:DFRRemovedCollateralCascade+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRRemovedCollateralCascade/Attribute:name" _delta="define"><![CDATA[Name]]></entry>
<entry id="Class:DFRRemovedCollateralCascade/Attribute:name+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRRemovedCollateralCascade/Attribute:extkey_id" _delta="define"><![CDATA[Dfrremovedcollateral id]]></entry>
<entry id="Class:DFRRemovedCollateralCascade/Attribute:extkey_id+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRManual/Name" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRManual/ComplementaryName" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRManual" _delta="define"><![CDATA[DFRManual]]></entry>
<entry id="Class:DFRManual+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRManual/Attribute:name" _delta="define"><![CDATA[Name]]></entry>
<entry id="Class:DFRManual/Attribute:name+" _delta="define"><![CDATA[]]></entry>
<entry id="Class:DFRManual/Attribute:extkey_id" _delta="define"><![CDATA[Dfrtoremove id]]></entry>
<entry id="Class:DFRManual/Attribute:extkey_id+" _delta="define"><![CDATA[]]></entry>
</entries>
</dictionary>
</dictionaries>
</itop_design>