Completed
Push — master ( ffa95e...df33b7 )
by Siad
13:09
created

VisualizerTask   A

Complexity

Total Complexity 38

Size/Duplication

Total Lines 414
Duplicated Lines 0 %

Test Coverage

Coverage 82.48%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 124
dl 0
loc 414
ccs 113
cts 137
cp 0.8248
rs 9.36
c 1
b 0
f 0
wmc 38

23 Methods

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