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 2
    public function init(): void
63
    {
64 2
        parent::init();
65 2
        if (!function_exists(\Jawira\PlantUml\encodep::class)) {
66 0
            $exceptionMessage = get_class($this) . ' requires "jawira/plantuml-encoding" library';
67
        }
68 2
        if (!class_exists(XSLTProcessor::class)) {
69 0
            $exceptionMessage = get_class($this) . ' requires XSL extension';
70
        }
71 2
        if (!class_exists(SimpleXMLElement::class)) {
72 0
            $exceptionMessage = get_class($this) . ' requires SimpleXML extension';
73
        }
74 2
        if (isset($exceptionMessage)) {
75 0
            $this->log($exceptionMessage, Project::MSG_ERR);
76 0
            throw new BuildException($exceptionMessage);
77
        }
78 2
        $this->setFormat(VisualizerTask::FORMAT_PNG);
79 2
        $this->setServer(VisualizerTask::SERVER);
80
    }
81

82
    /**
83
     * The main entry point method.
84
     *
85
     * @throws \GuzzleHttp\Exception\GuzzleException
86
     * @throws \IOException
87
     * @throws \NullPointerException
88
     */
89 2
    public function main(): void
90
    {
91 2
        $pumlDiagram = $this->generatePumlDiagram();
92 2
        $destination = $this->resolveImageDestination();
93 2
        $format = $this->getFormat();
94 2
        $image = $this->generateImage($pumlDiagram, $format);
95 2
        $this->saveToFile($image, $destination);
96
    }
97

98
    /**
99
     * Retrieves loaded buildfiles and generates a PlantUML diagram
100
     *
101
     * @return string
102
     */
103 2
    protected function generatePumlDiagram(): string
104
    {
105
        /**
106
         * @var \PhingXMLContext $xmlContext
107
         */
108 2
        $xmlContext = $this->getProject()
109 2
            ->getReference("phing.parsing.context");
110 2
        $importStack = $xmlContext->getImportStack();
111 2
        $pumlDiagram = $this->generatePuml($importStack);
112

113 2
        return $pumlDiagram;
114
    }
115

116
    /**
117
     * Read through provided buildfiles and generates a PlantUML diagram
118
     *
119
     * @param \PhingFile[] $buildFiles
120
     *
121
     * @return string
122
     */
123 2
    protected function generatePuml(array $buildFiles): string
124
    {
125 2
        $puml = $this->transformToPuml(reset($buildFiles), VisualizerTask::XSL_HEADER);
126

127
        /**
128
         * @var \PhingFile $buildFile
129
         */
130 2
        foreach ($buildFiles as $buildFile) {
131 2
            $puml .= $this->transformToPuml($buildFile, VisualizerTask::XSL_TARGETS);
132
        }
133

134 2
        foreach ($buildFiles as $buildFile) {
135 2
            $puml .= $this->transformToPuml($buildFile, VisualizerTask::XSL_CALLS);
136
        }
137

138 2
        $puml .= $this->transformToPuml(reset($buildFiles), VisualizerTask::XSL_FOOTER);
139

140 2
        return $puml;
141
    }
142

143
    /**
144
     * Transforms buildfile using provided xsl file
145
     *
146
     * @param \PhingFile $buildfile Path to buildfile
147
     * @param string $xslFile XSLT file
148
     *
149
     * @return string
150
     */
151 2
    protected function transformToPuml(PhingFile $buildfile, string $xslFile): string
152
    {
153 2
        $xml = $this->loadXmlFile($buildfile->getPath());
154 2
        $xsl = $this->loadXmlFile($xslFile);
155

156 2
        $processor = new XSLTProcessor();
157 2
        $processor->importStylesheet($xsl);
158

159 2
        return $processor->transformToXml($xml) . PHP_EOL;
160
    }
161

162
    /**
163
     * Load XML content from a file
164
     *
165
     * @param string $xmlFile XML or XSLT file
166
     *
167
     * @return \SimpleXMLElement
168
     */
169 2
    protected function loadXmlFile(string $xmlFile): SimpleXMLElement
170
    {
171 2
        $xmlContent = (new FileReader($xmlFile))->read();
172 2
        $xml = simplexml_load_string($xmlContent);
173

174 2
        if (!($xml instanceof SimpleXMLElement)) {
175 0
            $message = "Error loading XML file: $xmlFile";
176 0
            $this->log($message, Project::MSG_ERR);
177 0
            throw new BuildException($message);
178
        }
179

180 2
        return $xml;
181
    }
182

183
    /**
184
     * Get the image's final location
185
     *
186
     * @return \PhingFile
187
     * @throws \IOException
188
     * @throws \NullPointerException
189
     */
190 2
    protected function resolveImageDestination(): PhingFile
191
    {
192 2
        $phingFile = $this->getProject()->getProperty('phing.file');
193 2
        $format = $this->getFormat();
194 2
        $candidate = $this->getDestination();
195 2
        $path = $this->resolveDestination($phingFile, $format, $candidate);
196

197 2
        return new PhingFile($path);
198
    }
199

200
    /**
201
     * @return string
202
     */
203 2
    public function getFormat(): string
204
    {
205 2
        return $this->format;
206
    }
207

208
    /**
209
     * Sets and validates diagram's format
210
     *
211
     * @param string $format
212
     *
213
     * @return VisualizerTask
214
     */
215 2
    public function setFormat(string $format): VisualizerTask
216
    {
217 2
        switch ($format) {
218
            case VisualizerTask::FORMAT_PUML:
219
            case VisualizerTask::FORMAT_PNG:
220
            case VisualizerTask::FORMAT_EPS:
221
            case VisualizerTask::FORMAT_SVG:
222 2
                $this->format = $format;
223 2
                break;
224
            default:
225 2
                $message = "'$format' is not a valid format";
226 2
                $this->log($message, Project::MSG_ERR);
227 2
                throw new BuildException($message);
228
                break;
229
        }
230

231 2
        return $this;
232
    }
233

234
    /**
235
     * @return null|string
236
     */
237 2
    public function getDestination(): ?string
238
    {
239 2
        return $this->destination;
240
    }
241

242
    /**
243
     * @param string $destination
244
     *
245
     * @return VisualizerTask
246
     */
247 2
    public function setDestination(?string $destination): VisualizerTask
248
    {
249 2
        $this->destination = $destination;
250

251 2
        return $this;
252
    }
253

254
    /**
255
     * Figure diagram's file path
256
     *
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 2
    protected function resolveDestination(string $buildfilePath, string $format, ?string $destination): string
264
    {
265 2
        $buildfileInfo = pathinfo($buildfilePath);
266

267
        // Fallback
268 2
        if (empty($destination)) {
269 2
            $destination = $buildfileInfo['dirname'];
270
        }
271

272
        // Adding filename if necessary
273 2
        if (is_dir($destination)) {
274 2
            $destination .= DIRECTORY_SEPARATOR . $buildfileInfo['filename'] . '.' . $format;
275
        }
276

277
        // Check if path is available
278 2
        if (!is_dir(dirname($destination))) {
279 2
            $message = "Directory '$destination' is invalid";
280 2
            $this->log($message, Project::MSG_ERR);
281 2
            throw new BuildException(sprintf($message, $destination));
282
        }
283

284 2
        return $destination;
285
    }
286

287
    /**
288
     * Generates an actual image using PlantUML code
289
     *
290
     * @param string $pumlDiagram
291
     * @param string $format
292
     *
293
     * @return string
294
     * @throws \GuzzleHttp\Exception\GuzzleException
295
     */
296 2
    protected function generateImage(string $pumlDiagram, string $format): string
297
    {
298 2
        if ($format === VisualizerTask::FORMAT_PUML) {
299 2
            $this->log('Bypassing, no need to call server', Project::MSG_DEBUG);
300

301 2
            return $pumlDiagram;
302
        }
303

304 2
        $format = $this->getFormat();
305 2
        $encodedPuml = encodep($pumlDiagram);
306 2
        $this->prepareImageUrl($format, $encodedPuml);
307

308 0
        $response = $this->request();
309 0
        $this->processResponse($response); // used for status validation
310

311 0
        return $response->getBody()->getContents();
312
    }
313

314
    /**
315
     * Prepares URL from where image will be downloaded
316
     *
317
     * @param string $format
318
     * @param string $encodedPuml
319
     */
320 2
    protected function prepareImageUrl(string $format, string $encodedPuml): void
321
    {
322 2
        $server = $this->getServer();
323 2
        $this->log("Server: $server", Project::MSG_VERBOSE);
324

325 2
        $server = filter_var($server, FILTER_VALIDATE_URL);
326 2
        if ($server === false) {
327 2
            $message = 'Invalid PlantUml server';
328 2
            $this->log($message, Project::MSG_ERR);
329 2
            throw new BuildException($message);
330
        }
331

332 0
        $imageUrl = sprintf('%s/%s/%s', rtrim($server, '/'), $format, $encodedPuml);
333 0
        $this->log($imageUrl, Project::MSG_DEBUG);
334 0
        $this->setUrl($imageUrl);
335
    }
336

337
    /**
338
     * @return string
339
     */
340 2
    public function getServer(): string
341
    {
342 2
        return $this->server;
343
    }
344

345
    /**
346
     * @param string $server
347
     *
348
     * @return VisualizerTask
349
     */
350 2
    public function setServer(string $server): VisualizerTask
351
    {
352 2
        $this->server = $server;
353

354 2
        return $this;
355
    }
356

357
    /**
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 0
    protected function processResponse(\Psr\Http\Message\ResponseInterface $response): void
367
    {
368 0
        $status = $response->getStatusCode();
369 0
        $reasonPhrase = $response->getReasonPhrase();
370 0
        $this->log("Response status: $status", Project::MSG_DEBUG);
371 0
        $this->log("Response reason: $reasonPhrase", Project::MSG_DEBUG);
372

373 0
        if ($status !== VisualizerTask::STATUS_OK) {
374 0
            $message = "Request unsuccessful. Response from server: $status $reasonPhrase";
375 0
            $this->log($message, Project::MSG_ERR);
376 0
            throw new BuildException($message);
377
        }
378
    }
379

380
    /**
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 2
    protected function saveToFile(string $content, PhingFile $destination): void
389
    {
390 2
        $path = $destination->getPath();
391 2
        $this->log("Writing: $path", Project::MSG_INFO);
392

393 2
        (new FileWriter($destination))->write($content);
394
    }
395
}

Read our documentation on viewing source code .

Loading