Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/visualizer task #1019

Merged
merged 10 commits into from
Feb 2, 2019
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion classes/phing/tasks/defaults.properties
Original file line number Diff line number Diff line change
Expand Up @@ -196,4 +196,5 @@ phardata=phing.tasks.ext.phar.PharDataTask
sonar=phing.tasks.ext.sonar.SonarTask
hipchat=phing.tasks.ext.hipchat.HipchatTask
jsonvalidate=phing.tasks.ext.JsonValidateTask
phpstan=phing.tasks.ext.PHPStanTask
phpstan=phing.tasks.ext.PHPStanTask
visualizer=phing.tasks.ext.visualizer.VisualizerTask
367 changes: 367 additions & 0 deletions classes/phing/tasks/ext/visualizer/VisualizerTask.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,367 @@
<?php declare(strict_types=1);

use function Jawira\PlantUml\encodep;

/**
* Class VisualizerTask
*
* @author Jawira Portugal
*/
class VisualizerTask extends HttpTask
{
public const FORMAT_EPS = 'eps';
public const FORMAT_PNG = 'png';
public const FORMAT_PUML = 'puml';
public const FORMAT_SVG = 'svg';
public const SERVER = 'http://www.plantuml.com/plantuml';
public const STATUS_OK = 200;
public const XSL_CALLS = __DIR__ . '/calls.xsl';
public const XSL_FOOTER = __DIR__ . '/footer.xsl';
public const XSL_HEADER = __DIR__ . '/header.xsl';
public const XSL_TARGETS = __DIR__ . '/targets.xsl';

/**
* @var string Diagram format
*/
protected $format;

/**
* @var string Location in disk where diagram is saved
*/
protected $destination;

/**
* @var string PlantUml server
*/
protected $server;

/**
* The init method: Do init steps.
*/
public function init(): void
{
parent::init();
$this->setFormat(VisualizerTask::FORMAT_PNG);
$this->setServer(VisualizerTask::SERVER);
$this->checkXslExtension();
$this->checkXmlExtension();
}

protected function checkXslExtension(): void
{
$this->checkExtension('XSLTProcessor', 'Please install XSL extension');
}

/**
* Instead of checking the ext
*
* @param string $class Name of the class to verify
* @param string $message Error message to display when class don't exists
*/
protected function checkExtension(string $class, string $message): void
{
if (!class_exists($class)) {
$this->log($message, Project::MSG_ERR);
throw new BuildException($message);
}
}

protected function checkXmlExtension(): void
{
$this->checkExtension('SimpleXMLElement', 'Please install SimpleXML extension');
}

/**
* The main entry point method.
*
* @throws \HTTP_Request2_Exception
* @throws \IOException
* @throws \NullPointerException
*/
public function main(): void
{
$pumlDiagram = $this->generatePumlDiagram();
$destination = $this->resolveImageDestination();
$format = $this->getFormat();
$image = $this->generateImage($pumlDiagram, $format);
$this->saveToFile($image, $destination);
}

/**
* Read through provided buildfiles and generates a PlantUML diagram
*
* @return string
*/
protected function generatePumlDiagram(): string
{
/** @var \PhingXMLContext $xmlContext */
$xmlContext = $this->getProject()
->getReference("phing.parsing.context");
$importStack = $xmlContext->getImportStack();
$pumlDiagram = $this->generatePuml($importStack);

return $pumlDiagram;
}

/**
* @param \PhingFile[] $buildFiles
*
* @return string
*/
protected function generatePuml(array $buildFiles): string
{
$puml = $this->transformToPuml(reset($buildFiles), VisualizerTask::XSL_HEADER);

/** @var \PhingFile $buildFile */
foreach ($buildFiles as $buildFile) {
$puml .= $this->transformToPuml($buildFile, VisualizerTask::XSL_TARGETS);
}

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

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

return $puml;
}

/**
* Transforms buildfile using provided xsl file
*
* @param \PhingFile $buildfile Path to buildfile
* @param string $xslFile XSLT file
*
* @return string
*/
protected function transformToPuml(PhingFile $buildfile, string $xslFile): string
{
$xml = $this->loadXmlFile($buildfile->getPath());
$xsl = $this->loadXmlFile($xslFile);

$processor = new XSLTProcessor();
$processor->importStylesheet($xsl);

return $processor->transformToXml($xml) . PHP_EOL;
}

/**
* @param string $xmlFile XML or XSLT file
*
* @return \SimpleXMLElement
*/
protected function loadXmlFile(string $xmlFile): SimpleXMLElement
{
$xmlContent = (new FileReader($xmlFile))->read();
$xml = simplexml_load_string($xmlContent);

if (!($xml instanceof SimpleXMLElement)) {
$message = "Error loading XML file: $xmlFile";
$this->log($message, Project::MSG_ERR);
throw new BuildException($message);
}

return $xml;
}

/**
* @return \PhingFile
* @throws \IOException
* @throws \NullPointerException
*/
protected function resolveImageDestination(): PhingFile
{
$phingFile = $this->getProject()->getProperty('phing.file');
$format = $this->getFormat();
$candidate = $this->getDestination();
$path = $this->resolveDestination($phingFile, $format, $candidate);

return new PhingFile($path);
}

/**
* @return string
*/
public function getFormat(): string
{
return $this->format;
}

/**
* @param string $format
*
* @return VisualizerTask
*/
public function setFormat(string $format): VisualizerTask
{
switch ($format) {
case VisualizerTask::FORMAT_PUML:
case VisualizerTask::FORMAT_PNG:
case VisualizerTask::FORMAT_EPS:
case VisualizerTask::FORMAT_SVG:
$this->format = $format;
break;
default:
$message = "'$format' is not a valid format";
$this->log($message, Project::MSG_ERR);
throw new BuildException($message);
break;
}

return $this;
}

/**
* @return null|string
*/
public function getDestination(): ?string
{
return $this->destination;
}

/**
* @param string $destination
*
* @return VisualizerTask
*/
public function setDestination(?string $destination): VisualizerTask
{
$this->destination = $destination;

return $this;
}

/**
* @param string $buildfilePath Path to main buildfile
* @param string $format Extension to use
* @param null|string $destination Desired destination provided by user
*
* @return string
*/
protected function resolveDestination(string $buildfilePath, string $format, ?string $destination): string
{
$buildfileInfo = pathinfo($buildfilePath);

// Fallback
if (empty($destination)) {
$destination = $buildfileInfo['dirname'];
}

// Adding filename if necessary
if (is_dir($destination)) {
$destination .= DIRECTORY_SEPARATOR . $buildfileInfo['filename'] . '.' . $format;
}

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

return $destination;
}

/**
* Generates an actual image using PlantUML code
*
* @param string $pumlDiagram
* @param string $format
*
* @return string
* @throws \HTTP_Request2_Exception
*/
protected function generateImage(string $pumlDiagram, string $format): string
{
if ($format === VisualizerTask::FORMAT_PUML) {
$this->log('Bypassing, no need to call server', Project::MSG_DEBUG);

return $pumlDiagram;
}

$format = $this->getFormat();
$encodedPuml = encodep($pumlDiagram);
$this->prepareImageUrl($format, $encodedPuml);

$response = $this->createRequest()->send();
$this->processResponse($response); // used for status validation

return $response->getBody();
}

protected function prepareImageUrl(string $format, string $encodedPuml): void
{
$server = $this->getServer();
$this->log("Server: $server", Project::MSG_VERBOSE);

$server = filter_var($server, FILTER_VALIDATE_URL);
if ($server === false) {
$message = 'Invalid PlantUml server';
$this->log($message, Project::MSG_ERR);
throw new BuildException($message);
}

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

/**
* @return string
*/
public function getServer(): string
{
return $this->server;
}

/**
* @param string $server
*
* @return VisualizerTask
*/
public function setServer(string $server): VisualizerTask
{
$this->server = $server;

return $this;
}

/**
* Receive server's response
*
* This method validates $response's status
*
* @param HTTP_Request2_Response $response Response from server
*
* @return void
*/
protected function processResponse(HTTP_Request2_Response $response): void
{
$status = $response->getStatus();
$reasonPhrase = $response->getReasonPhrase();
$this->log("Response status: $status", Project::MSG_DEBUG);
$this->log("Response reason: $reasonPhrase", Project::MSG_DEBUG);

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

/**
* Save provided $content string into $destination file
*
* @param string $content Content to save
* @param \PhingFile $destination Location where $content is saved
*
* @return void
*/
protected function saveToFile(string $content, PhingFile $destination): void
{
$path = $destination->getPath();
$this->log("Writing: $path", Project::MSG_INFO);

(new FileWriter($destination))->write($content);
}
}
Loading