Passed
Push — master ( 06668f...038ad1 )
by Siad
23:30
created

VisualizerTask::processResponse()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 8
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 11
ccs 4
cts 4
cp 1
crap 2
rs 10
1
<?php
2
/**
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the LGPL. For more information please see
17
 * <http://phing.info>.
18
 */
19
20
declare(strict_types=1);
21
22
use function Jawira\PlantUml\encodep;
23
24
/**
25
 * Class VisualizerTask
26
 *
27
 * VisualizerTask creates diagrams using buildfiles, these diagrams represents calls and depends among targets.
28
 *
29
 * @author Jawira Portugal
30
 */
31
class VisualizerTask extends HttpTask
32
{
33
    public const FORMAT_EPS = 'eps';
34
    public const FORMAT_PNG = 'png';
35
    public const FORMAT_PUML = 'puml';
36
    public const FORMAT_SVG = 'svg';
37
    public const SERVER = 'http://www.plantuml.com/plantuml';
38
    public const STATUS_OK = 200;
39
    public const XSL_CALLS = __DIR__ . '/calls.xsl';
40
    public const XSL_FOOTER = __DIR__ . '/footer.xsl';
41
    public const XSL_HEADER = __DIR__ . '/header.xsl';
42
    public const XSL_TARGETS = __DIR__ . '/targets.xsl';
43
44
    /**
45
     * @var string Diagram format
46
     */
47
    protected $format;
48
49
    /**
50
     * @var string Location in disk where diagram is saved
51
     */
52
    protected $destination;
53
54
    /**
55
     * @var string PlantUml server
56
     */
57
    protected $server;
58
59
    /**
60
     * Setting some default values and checking requirements
61
     */
62 6
    public function init(): void
63
    {
64 6
        parent::init();
65 6
        if (!function_exists(\Jawira\PlantUml\encodep::class)) {
0 ignored issues
show
Bug introduced by
The type Jawira\PlantUml\encodep was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
66 6
            $exceptionMessage = get_class($this) . ' requires "jawira/plantuml-encoding" library';
67 6
        }
68 6
        if (!class_exists(XSLTProcessor::class)) {
69 6
            $exceptionMessage = get_class($this) . ' requires XSL extension';
70 6
        }
71
        if (!class_exists(SimpleXMLElement::class)) {
72
            $exceptionMessage = get_class($this) . ' requires SimpleXML extension';
73
        }
74
        if (isset($exceptionMessage)) {
75
            $this->log($exceptionMessage, Project::MSG_ERR);
76
            throw new BuildException($exceptionMessage);
77 6
        }
78
        $this->setFormat(VisualizerTask::FORMAT_PNG);
79 6
        $this->setServer(VisualizerTask::SERVER);
80 6
    }
81
82
    /**
83
     * The main entry point method.
84
     *
85
     * @throws \GuzzleHttp\Exception\GuzzleException
86
     * @throws \IOException
87
     * @throws \NullPointerException
88 6
     */
89
    public function main(): void
90 6
    {
91
        $pumlDiagram = $this->generatePumlDiagram();
92
        $destination = $this->resolveImageDestination();
93
        $format = $this->getFormat();
94 6
        $image = $this->generateImage($pumlDiagram, $format);
95
        $this->saveToFile($image, $destination);
96
    }
97
98
    /**
99
     * Retrieves loaded buildfiles and generates a PlantUML diagram
100
     *
101
     * @return string
102 6
     */
103
    protected function generatePumlDiagram(): string
104 6
    {
105 6
        /**
106
         * @var \PhingXMLContext $xmlContext
107 6
         */
108
        $xmlContext = $this->getProject()
109
            ->getReference("phing.parsing.context");
110
        $importStack = $xmlContext->getImportStack();
111 6
        $pumlDiagram = $this->generatePuml($importStack);
112
113
        return $pumlDiagram;
114
    }
115
116
    /**
117
     * Read through provided buildfiles and generates a PlantUML diagram
118 6
     *
119
     * @param \PhingFile[] $buildFiles
120 6
     *
121 6
     * @return string
122
     */
123
    protected function generatePuml(array $buildFiles): string
124
    {
125
        $puml = $this->transformToPuml(reset($buildFiles), VisualizerTask::XSL_HEADER);
126
127
        /**
128 6
         * @var \PhingFile $buildFile
129
         */
130 6
        foreach ($buildFiles as $buildFile) {
131 6
            $puml .= $this->transformToPuml($buildFile, VisualizerTask::XSL_TARGETS);
132
        }
133
134
        foreach ($buildFiles as $buildFile) {
135
            $puml .= $this->transformToPuml($buildFile, VisualizerTask::XSL_CALLS);
136
        }
137
138
        $puml .= $this->transformToPuml(reset($buildFiles), VisualizerTask::XSL_FOOTER);
139
140 5
        return $puml;
141
    }
142 5
143 5
    /**
144 4
     * Transforms buildfile using provided xsl file
145 4
     *
146 3
     * @param \PhingFile $buildfile Path to buildfile
147 3
     * @param string $xslFile XSLT file
148
     *
149
     * @return string
150
     */
151
    protected function transformToPuml(PhingFile $buildfile, string $xslFile): string
152
    {
153
        $xml = $this->loadXmlFile($buildfile->getPath());
154 5
        $xsl = $this->loadXmlFile($xslFile);
155
156
        $processor = new XSLTProcessor();
157
        $processor->importStylesheet($xsl);
158
159 5
        return $processor->transformToXml($xml) . PHP_EOL;
160 5
    }
161 5
162 5
    /**
163
     * Load XML content from a file
164 5
     *
165
     * @param string $xmlFile XML or XSLT file
166
     *
167
     * @return \SimpleXMLElement
168
     */
169
    protected function loadXmlFile(string $xmlFile): SimpleXMLElement
170
    {
171
        $xmlContent = (new FileReader($xmlFile))->read();
172
        $xml = simplexml_load_string($xmlContent);
173
174 5
        if (!($xml instanceof SimpleXMLElement)) {
175
            $message = "Error loading XML file: $xmlFile";
176 5
            $this->log($message, Project::MSG_ERR);
177
            throw new BuildException($message);
178
        }
179
180
        return $xml;
181 5
    }
182 5
183
    /**
184
     * Get the image's final location
185 5
     *
186 5
     * @return \PhingFile
187
     * @throws \IOException
188
     * @throws \NullPointerException
189 5
     */
190
    protected function resolveImageDestination(): PhingFile
191 5
    {
192
        $phingFile = $this->getProject()->getProperty('phing.file');
193
        $format = $this->getFormat();
194
        $candidate = $this->getDestination();
195
        $path = $this->resolveDestination($phingFile, $format, $candidate);
196
197
        return new PhingFile($path);
198
    }
199
200
    /**
201
     * @return string
202 5
     */
203
    public function getFormat(): string
204 5
    {
205 5
        return $this->format;
206
    }
207 5
208 5
    /**
209
     * Sets and validates diagram's format
210 5
     *
211
     * @param string $format
212
     *
213
     * @return VisualizerTask
214
     */
215
    public function setFormat(string $format): VisualizerTask
216
    {
217
        switch ($format) {
218
            case VisualizerTask::FORMAT_PUML:
219
            case VisualizerTask::FORMAT_PNG:
220 5
            case VisualizerTask::FORMAT_EPS:
221
            case VisualizerTask::FORMAT_SVG:
222 5
                $this->format = $format;
223 5
                break;
224
            default:
225 5
                $message = "'$format' is not a valid format";
226
                $this->log($message, Project::MSG_ERR);
227
                throw new BuildException($message);
228
                break;
229
        }
230
231 5
        return $this;
232
    }
233
234
    /**
235
     * @return null|string
236
     */
237
    public function getDestination(): ?string
238
    {
239
        return $this->destination;
240
    }
241 5
242
    /**
243 5
     * @param string $destination
244 5
     *
245 5
     * @return VisualizerTask
246 5
     */
247
    public function setDestination(?string $destination): VisualizerTask
248 4
    {
249
        $this->destination = $destination;
250
251
        return $this;
252
    }
253
254 5
    /**
255
     * Figure diagram's file path
256 5
     *
257
     * @param string $buildfilePath Path to main buildfile
258
     * @param string $format Extension to use
259
     * @param null|string $destination Desired destination provided by user
260
     *
261
     * @return string
262
     */
263
    protected function resolveDestination(string $buildfilePath, string $format, ?string $destination): string
264
    {
265
        $buildfileInfo = pathinfo($buildfilePath);
266 6
267
        // Fallback
268 6
        if (empty($destination)) {
269
            $destination = $buildfileInfo['dirname'];
270
        }
271
272
        // Adding filename if necessary
273 6
        if (is_dir($destination)) {
274 6
            $destination .= DIRECTORY_SEPARATOR . $buildfileInfo['filename'] . '.' . $format;
275
        }
276 1
277 1
        // Check if path is available
278 1
        if (!is_dir(dirname($destination))) {
279
            $message = "Directory '$destination' is invalid";
280
            $this->log($message, Project::MSG_ERR);
281
            throw new BuildException(sprintf($message, $destination));
282 6
        }
283
284
        return $destination;
285
    }
286
287
    /**
288 5
     * Generates an actual image using PlantUML code
289
     *
290 5
     * @param string $pumlDiagram
291
     * @param string $format
292
     *
293
     * @return string
294
     * @throws \GuzzleHttp\Exception\GuzzleException
295
     */
296
    protected function generateImage(string $pumlDiagram, string $format): string
297
    {
298 3
        if ($format === VisualizerTask::FORMAT_PUML) {
299
            $this->log('Bypassing, no need to call server', Project::MSG_DEBUG);
300 3
301
            return $pumlDiagram;
302 3
        }
303
304
        $format = $this->getFormat();
305
        $encodedPuml = encodep($pumlDiagram);
306
        $this->prepareImageUrl($format, $encodedPuml);
307
308
        $response = $this->request();
309
        $this->processResponse($response); // used for status validation
310
311
        return $response->getBody()->getContents();
312
    }
313
314 5
    /**
315
     * Prepares URL from where image will be downloaded
316 5
     *
317
     * @param string $format
318
     * @param string $encodedPuml
319 5
     */
320 2
    protected function prepareImageUrl(string $format, string $encodedPuml): void
321
    {
322
        $server = $this->getServer();
323
        $this->log("Server: $server", Project::MSG_VERBOSE);
324 5
325 3
        $server = filter_var($server, FILTER_VALIDATE_URL);
326
        if ($server === false) {
327
            $message = 'Invalid PlantUml server';
328
            $this->log($message, Project::MSG_ERR);
329 5
            throw new BuildException($message);
330 1
        }
331 1
332 1
        $imageUrl = sprintf('%s/%s/%s', rtrim($server, '/'), $format, $encodedPuml);
333
        $this->log($imageUrl, Project::MSG_DEBUG);
334
        $this->setUrl($imageUrl);
335 4
    }
336
337
    /**
338
     * @return string
339
     */
340
    public function getServer(): string
341
    {
342
        return $this->server;
343
    }
344
345
    /**
346
     * @param string $server
347 4
     *
348
     * @return VisualizerTask
349 4
     */
350 3
    public function setServer(string $server): VisualizerTask
351
    {
352 3
        $this->server = $server;
353
354
        return $this;
355 1
    }
356 1
357 1
    /**
358
     * Receive server's response
359
     *
360
     * This method validates `$response`'s status
361
     *
362
     * @param \Psr\Http\Message\ResponseInterface $response Response from server
363
     *
364
     * @return void
365
     */
366
    protected function processResponse(\Psr\Http\Message\ResponseInterface $response): void
367
    {
368
        $status = $response->getStatusCode();
369
        $reasonPhrase = $response->getReasonPhrase();
370
        $this->log("Response status: $status", Project::MSG_DEBUG);
371 1
        $this->log("Response reason: $reasonPhrase", Project::MSG_DEBUG);
372
373 1
        if ($status !== VisualizerTask::STATUS_OK) {
374 1
            $message = "Request unsuccessful. Response from server: $status $reasonPhrase";
375
            $this->log($message, Project::MSG_ERR);
376 1
            throw new BuildException($message);
377 1
        }
378 1
    }
379 1
380 1
    /**
381
     * Save provided $content string into $destination file
382
     *
383
     * @param string $content Content to save
384
     * @param \PhingFile $destination Location where $content is saved
385
     *
386
     * @return void
387
     */
388
    protected function saveToFile(string $content, PhingFile $destination): void
389
    {
390
        $path = $destination->getPath();
391 1
        $this->log("Writing: $path", Project::MSG_INFO);
392
393 1
        (new FileWriter($destination))->write($content);
394
    }
395
}
396