Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
76 / 76
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
1 / 1
HeaderValidator
100.00% covered (success)
100.00%
76 / 76
100.00% covered (success)
100.00%
7 / 7
24
100.00% covered (success)
100.00%
1 / 1
 validate
100.00% covered (success)
100.00%
43 / 43
100.00% covered (success)
100.00%
1 / 1
7
 isRequired
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMinOccurrences
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getMissingMatches
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
6
 getSearchMatch
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getMatchHeader
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 findOccurrences
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
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 (under the project TAO-PRODUCT);
19 */
20
21declare(strict_types=1);
22
23namespace oat\taoQtiItem\model\import\Validator;
24
25use oat\oatbox\service\ConfigurableService;
26use oat\taoQtiItem\model\import\Decorator\CvsToQtiTemplateDecorator;
27use oat\taoQtiItem\model\import\TemplateInterface;
28
29class HeaderValidator extends ConfigurableService implements ValidatorInterface
30{
31    /**
32     * @inheritDoc
33     */
34    public function validate(array $content, TemplateInterface $csvTemplate): void
35    {
36        $logger = $this->getLogger();
37        $decorator = new CvsToQtiTemplateDecorator($csvTemplate);
38
39        $validationConfig = $decorator->getCsvColumns();
40
41        $errors = [];
42
43        foreach ($validationConfig as $headerRegex => $validations) {
44            $validations = explode('|', $validationConfig[$headerRegex]['header']);
45
46            $isRequired = $this->isRequired($validations);
47            $minOccurrences = $this->getMinOccurrences($validations);
48            $occurrences = $this->findOccurrences($content, $headerRegex);
49            $totalOccurrences = count($occurrences);
50
51            foreach ($this->getMissingMatches($headerRegex, $validations, $content, $occurrences) as $missingMatch) {
52                $errors[] = new ErrorValidationException(
53                    'Header `%s` is required',
54                    [
55                        $missingMatch,
56                    ]
57                );
58                $logger->debug(sprintf('Tabular import: Header `%s` is required', $missingMatch));
59            }
60
61            if ($isRequired && $totalOccurrences === 0) {
62                $errors[] = new ErrorValidationException(
63                    'Header `%s` is required',
64                    [
65                        $headerRegex
66                    ]
67                );
68                $logger->debug(sprintf('Tabular import: Header `%s` is required', $headerRegex));
69            }
70
71            if ($totalOccurrences < $minOccurrences) {
72                $errors[] = new ErrorValidationException(
73                    'Header `%s` must be provided at least `%s` times',
74                    [
75                        $headerRegex,
76                        $minOccurrences,
77                    ]
78                );
79
80                $logger->debug(
81                    sprintf(
82                        'Tabular import: Header `%s` must be provided at least `%s` times',
83                        $headerRegex,
84                        $minOccurrences
85                    )
86                );
87            }
88        }
89
90        if (count($errors) > 0) {
91            throw new AggregatedValidationException($errors, []);
92        }
93    }
94
95    private function isRequired(array $validations): bool
96    {
97        return in_array('required', $validations);
98    }
99
100    private function getMinOccurrences(array $validations): int
101    {
102        foreach ($validations as $validation) {
103            if (strpos($validation, 'min_occurrences:') !== false) {
104                return (int)str_replace('min_occurrences:', '', $validation);
105            }
106        }
107
108        return 0;
109    }
110
111    private function getMissingMatches(
112        string $headerRegex,
113        array $validations,
114        array $headers,
115        array $occurrences
116    ): array {
117        $matchHeader = $this->getMatchHeader($validations);
118
119        if ($matchHeader === null) {
120            return [];
121        }
122
123        $missingColumns = [];
124
125        foreach ($occurrences as $occurrence) {
126            $search = $this->getSearchMatch($headerRegex, $matchHeader, $occurrence);
127            $found = false;
128
129            foreach ($headers as $header) {
130                if ($header === $search) {
131                    $found = true;
132
133                    break;
134                }
135            }
136
137            if (!$found) {
138                $missingColumns[] = $search;
139            }
140        }
141
142        return $missingColumns;
143    }
144
145    private function getSearchMatch(string $headerRegex, string $matchHeader, string $occurrence): string
146    {
147        $open = strpos($headerRegex, '[');
148        $close = strpos($headerRegex, ']');
149        $regex = substr($headerRegex, $open + 1, $close - $open - 1);
150        $number = (int)preg_replace('/[^' . $regex . ']/', '', $occurrence);
151
152        return str_replace('[' . $regex . ']', (string)$number, $matchHeader);
153    }
154
155    private function getMatchHeader(array $validations): ?string
156    {
157        foreach ($validations as $validation) {
158            if (strpos($validation, 'match_header:') !== false) {
159                return str_replace('match_header:', '', $validation);
160            }
161        }
162
163        return null;
164    }
165
166    private function findOccurrences(array $header, string $headerRegex): array
167    {
168        $occurrences = [];
169
170        foreach ($header as $headerName) {
171            if (preg_match('/\b(' . $headerRegex . ')\b/', (string)$headerName) === 1) {
172                $occurrences[] = $headerName;
173            }
174        }
175
176        return $occurrences;
177    }
178}