Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.14% covered (warning)
86.14%
87 / 101
72.73% covered (warning)
72.73%
8 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
ClassDeleter
86.14% covered (warning)
86.14%
87 / 101
72.73% covered (warning)
72.73%
8 / 11
37.08
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 delete
54.55% covered (warning)
54.55%
12 / 22
0.00% covered (danger)
0.00%
0 / 1
5.50
 deleteClassRecursively
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 deleteClass
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 deleteClassContent
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 deleteInstances
88.00% covered (warning)
88.00%
22 / 25
0.00% covered (danger)
0.00%
0 / 1
7.08
 deleteProperties
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 deleteProperty
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
3.00
 defineResourceType
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 createQuery
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 filterInstances
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3/**
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; under version 2
7 * of the License (non-upgradable).
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 *
18 * Copyright (c) 2021 (original work) Open Assessment Technologies SA;
19 */
20
21declare(strict_types=1);
22
23namespace oat\tao\model\resources\Service;
24
25use oat\tao\model\resources\relation\FindAllQuery;
26use oat\tao\model\resources\relation\ResourceRelationCollection;
27use oat\tao\model\resources\relation\service\ResourceRelationServiceProxy;
28use oat\tao\model\TaoOntology;
29use Throwable;
30use core_kernel_classes_Class;
31use core_kernel_classes_Property;
32use oat\generis\model\data\Ontology;
33use oat\tao\model\search\index\OntologyIndex;
34use oat\tao\model\accessControl\PermissionCheckerInterface;
35use oat\tao\model\resources\Contract\ClassDeleterInterface;
36use oat\tao\model\Specification\ClassSpecificationInterface;
37use oat\tao\model\resources\Exception\ClassDeletionException;
38use oat\generis\model\resource\Context\ResourceRepositoryContext;
39use oat\generis\model\resource\Contract\ResourceRepositoryInterface;
40use oat\tao\model\resources\Exception\PartialClassDeletionException;
41
42class ClassDeleter implements ClassDeleterInterface
43{
44    private const RELATION_RESOURCE_MAP = [
45        TaoOntology::CLASS_URI_ITEM => 'itemClass'
46    ];
47    private const PROPERTY_INDEX = OntologyIndex::PROPERTY_INDEX;
48
49    /** @var ClassSpecificationInterface */
50    private $rootClassSpecification;
51
52    /** @var PermissionCheckerInterface */
53    private $permissionChecker;
54
55    /** @var Ontology */
56    private $ontology;
57
58    /** @var ResourceRepositoryInterface */
59    private $resourceRepository;
60
61    /** @var ResourceRepositoryInterface */
62    private $classRepository;
63
64    /** @var core_kernel_classes_Property */
65    private $propertyIndex;
66
67    /** @var core_kernel_classes_Class|null */
68    private $selectedClass;
69    private ResourceRelationServiceProxy $resourceRelationServiceProxy;
70
71    public function __construct(
72        ClassSpecificationInterface $rootClassSpecification,
73        PermissionCheckerInterface $permissionChecker,
74        Ontology $ontology,
75        ResourceRepositoryInterface $resourceRepository,
76        ResourceRepositoryInterface $classRepository,
77        ResourceRelationServiceProxy $resourceRelationServiceProxy
78    ) {
79        $this->rootClassSpecification = $rootClassSpecification;
80        $this->permissionChecker = $permissionChecker;
81        $this->ontology = $ontology;
82        $this->resourceRepository = $resourceRepository;
83        $this->classRepository = $classRepository;
84        $this->resourceRelationServiceProxy = $resourceRelationServiceProxy;
85
86        $this->propertyIndex = $ontology->getProperty(self::PROPERTY_INDEX);
87    }
88
89    public function delete(core_kernel_classes_Class $class): void
90    {
91        if ($this->rootClassSpecification->isSatisfiedBy($class)) {
92            throw new ClassDeletionException(
93                'The class provided for deletion cannot be the root class.',
94                __('You cannot delete the root node')
95            );
96        }
97
98        try {
99            $this->selectedClass = $class;
100            $this->deleteClassRecursively($class);
101        } catch (Throwable $exception) {
102            throw new PartialClassDeletionException(
103                sprintf(
104                    'Unable to delete class "%s::%s" (%s).',
105                    $class->getLabel(),
106                    $class->getUri(),
107                    $exception->getMessage()
108                ),
109                __('Unable to delete the selected resource')
110            );
111        }
112
113        if ($class->exists()) {
114            throw new PartialClassDeletionException(
115                'Some of resources has not be deleted',
116                __('Some of resources has not be deleted')
117            );
118        }
119    }
120
121    private function deleteClassRecursively(core_kernel_classes_Class $class): bool
122    {
123        if (!$this->permissionChecker->hasReadAccess($class->getUri())) {
124            return false;
125        }
126
127        $isClassDeletable = true;
128
129        foreach ($class->getSubClasses() as $subClass) {
130            $isClassDeletable = $this->deleteClassRecursively($subClass) && $isClassDeletable;
131        }
132
133        return $this->deleteClass($class, $isClassDeletable);
134    }
135
136    /**
137     * @param bool $isClassDeletable Class is not deletable if it contains at least one protected subclass,
138     *                               instance or property
139     */
140    private function deleteClass(core_kernel_classes_Class $class, bool $isClassDeletable): bool
141    {
142        if ($this->deleteClassContent($class, $isClassDeletable)) {
143            $this->classRepository->delete(
144                new ResourceRepositoryContext(
145                    [
146                        ResourceRepositoryContext::PARAM_CLASS => $class,
147                        ResourceRepositoryContext::PARAM_SELECTED_CLASS => $this->selectedClass,
148                    ]
149                )
150            );
151
152            return true;
153        }
154
155        return false;
156    }
157
158    private function deleteClassContent(core_kernel_classes_Class $class, bool $isClassDeletable): bool
159    {
160        return $this->deleteInstances($class)
161            && $isClassDeletable
162            && $this->permissionChecker->hasWriteAccess($class->getUri())
163            && $this->deleteProperties($class->getProperties());
164    }
165
166    private function deleteInstances(core_kernel_classes_Class $class): bool
167    {
168        $status = true;
169        $resources = $class->getInstances();
170        if ($query = $this->createQuery($class)) {
171            $itemsInUse = $this->resourceRelationServiceProxy->findRelations($query);
172            if ($itemsInUse->jsonSerialize()) {
173                $resources = $this->filterInstances($resources, $itemsInUse);
174                $status = false;
175            }
176        }
177
178        foreach ($resources as $instance) {
179            if (!$instance->exists()) {
180                continue;
181            }
182
183            if (!$this->permissionChecker->hasWriteAccess($instance->getUri())) {
184                $status = false;
185
186                continue;
187            }
188
189            try {
190                $this->resourceRepository->delete(
191                    new ResourceRepositoryContext(
192                        [
193                            ResourceRepositoryContext::PARAM_RESOURCE => $instance,
194                            ResourceRepositoryContext::PARAM_SELECTED_CLASS => $this->selectedClass,
195                            ResourceRepositoryContext::PARAM_PARENT_CLASS => $class,
196                        ]
197                    )
198                );
199            } catch (Throwable $exception) {
200                $status = false;
201            }
202        }
203
204        return $status;
205    }
206
207    /**
208     * @param core_kernel_classes_Property[] $properties
209     */
210    private function deleteProperties(array $properties): bool
211    {
212        $status = true;
213
214        foreach ($properties as $property) {
215            $status = $this->deleteProperty($property) && $status;
216        }
217
218        return $status;
219    }
220
221    private function deleteProperty(core_kernel_classes_Property $property): bool
222    {
223        $indexes = $property->getPropertyValues($this->propertyIndex);
224
225        if (!$property->delete(true)) {
226            return false;
227        }
228
229        foreach ($indexes as $indexUri) {
230            $this->resourceRepository->delete(
231                new ResourceRepositoryContext(
232                    [
233                        ResourceRepositoryContext::PARAM_RESOURCE => $this->ontology->getResource($indexUri),
234                        ResourceRepositoryContext::PARAM_DELETE_REFERENCE => true,
235                    ]
236                )
237            );
238        }
239
240        return true;
241    }
242
243    private function defineResourceType(core_kernel_classes_Class $class): ?string
244    {
245        if (isset(self::RELATION_RESOURCE_MAP[$class->getRootId()])) {
246            return self::RELATION_RESOURCE_MAP[$class->getRootId()];
247        }
248
249        return null;
250    }
251
252    private function createQuery($class): ?FindAllQuery
253    {
254        if ($this->defineResourceType($class)) {
255            return new FindAllQuery(null, $class->getUri(), $this->defineResourceType($class));
256        }
257
258        return null;
259    }
260
261    private function filterInstances(array $resourceCollection, ResourceRelationCollection $itemsInUse): iterable
262    {
263        foreach ($itemsInUse->getIterator() as $item) {
264            unset($resourceCollection[$item->getId()]);
265        }
266
267        return $resourceCollection;
268    }
269}