From 1e090476a16151e7d7d09b9cc4704528d4c49326 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tobias=20St=C3=B6ckler?= <tobiasstoeckler@googlemail.com>
Date: Tue, 24 May 2016 12:59:52 +0200
Subject: [PATCH] by tstoeckler: Implement version detection for libraries

---
 libraries.services.yml                        |   3 +
 src/Annotation/VersionDetector.php            |  14 ++
 src/ExternalLibrary/Asset/AssetLibrary.php    |  14 +-
 .../Asset/LocalRemoteAssetTrait.php           |   4 -
 .../Asset/SingleAssetLibraryTrait.php         |   1 +
 .../LibraryNotInstalledException.php          |   2 +-
 .../UnknownLibraryVersionException.php        |  38 ++++
 src/ExternalLibrary/LibraryInterface.php      |   8 -
 src/ExternalLibrary/LibraryTrait.php          |  13 --
 .../Version/VersionDetectorInterface.php      |  23 +++
 .../Version/VersionDetectorManager.php        |  36 ++++
 .../Version/VersionedLibraryInterface.php     |  55 +++++
 .../Version/VersionedLibraryTrait.php         |  85 ++++++++
 .../LibraryType/AssetLibraryType.php          |  22 +-
 .../LibraryType/PhpFileLibraryType.php        |  20 +-
 .../VersionDetector/LinePatternDetector.php   |  86 ++++++++
 .../VersionDetector/StaticDetector.php        |  45 ++++
 .../test_asset_library.yml                    |   4 +
 .../Asset/AssetLibraryTest.php                |  19 +-
 .../PhpFile/PhpFileLibraryTest.php            |   5 -
 .../TestLibraryFilesStream.php                |   5 -
 tests/src/Kernel/LibraryKernelTestBase.php    |   5 -
 .../LinePatternDetectorTest.php               | 195 ++++++++++++++++++
 23 files changed, 642 insertions(+), 60 deletions(-)
 create mode 100644 src/Annotation/VersionDetector.php
 create mode 100644 src/ExternalLibrary/Exception/UnknownLibraryVersionException.php
 create mode 100644 src/ExternalLibrary/Version/VersionDetectorInterface.php
 create mode 100644 src/ExternalLibrary/Version/VersionDetectorManager.php
 create mode 100644 src/ExternalLibrary/Version/VersionedLibraryInterface.php
 create mode 100644 src/ExternalLibrary/Version/VersionedLibraryTrait.php
 create mode 100644 src/Plugin/libraries/VersionDetector/LinePatternDetector.php
 create mode 100644 src/Plugin/libraries/VersionDetector/StaticDetector.php
 create mode 100644 tests/src/Unit/Plugin/libraries/VersionDetector/LinePatternDetectorTest.php

diff --git a/libraries.services.yml b/libraries.services.yml
index 3f463c8..1ee8a6f 100644
--- a/libraries.services.yml
+++ b/libraries.services.yml
@@ -15,6 +15,9 @@ services:
   plugin.manager.libraries.locator:
     class: Drupal\libraries\ExternalLibrary\Local\LocatorManager
     parent: default_plugin_manager
+  plugin.manager.libraries.version_detector:
+    class: Drupal\libraries\ExternalLibrary\Version\VersionDetectorManager
+    parent: default_plugin_manager
 
   libraries.extension_handler:
     class: Drupal\libraries\Extension\ExtensionHandler
diff --git a/src/Annotation/VersionDetector.php b/src/Annotation/VersionDetector.php
new file mode 100644
index 0000000..a664f99
--- /dev/null
+++ b/src/Annotation/VersionDetector.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Drupal\libraries\Annotation;
+
+use Drupal\Component\Annotation\PluginID;
+
+/**
+ * Provides an annotation class for version detector plugins.
+ *
+ * @Annotation
+ */
+class VersionDetector extends PluginID {
+
+}
diff --git a/src/ExternalLibrary/Asset/AssetLibrary.php b/src/ExternalLibrary/Asset/AssetLibrary.php
index dedb5a9..dcfd312 100644
--- a/src/ExternalLibrary/Asset/AssetLibrary.php
+++ b/src/ExternalLibrary/Asset/AssetLibrary.php
@@ -7,23 +7,27 @@
 
 namespace Drupal\libraries\ExternalLibrary\Asset;
 
+use Drupal\libraries\ExternalLibrary\Exception\UnknownLibraryVersionException;
 use Drupal\libraries\ExternalLibrary\LibraryTrait;
 use Drupal\libraries\ExternalLibrary\Local\LocalLibraryInterface;
 use Drupal\libraries\ExternalLibrary\Local\LocalLibraryTrait;
 use Drupal\libraries\ExternalLibrary\Remote\RemoteLibraryInterface;
 use Drupal\libraries\ExternalLibrary\Remote\RemoteLibraryTrait;
+use Drupal\libraries\ExternalLibrary\Version\VersionedLibraryInterface;
+use Drupal\libraries\ExternalLibrary\Version\VersionedLibraryTrait;
 
 /**
  * Provides a base asset library implementation.
  */
-class AssetLibrary implements AssetLibraryInterface, LocalLibraryInterface, RemoteLibraryInterface {
+class AssetLibrary implements AssetLibraryInterface, VersionedLibraryInterface, LocalLibraryInterface, RemoteLibraryInterface {
 
   use
     LibraryTrait,
     LocalLibraryTrait,
     RemoteLibraryTrait,
     SingleAssetLibraryTrait,
-    LocalRemoteAssetTrait
+    LocalRemoteAssetTrait,
+    VersionedLibraryTrait
   ;
 
   /**
@@ -36,7 +40,11 @@ class AssetLibrary implements AssetLibraryInterface, LocalLibraryInterface, Remo
    */
   public function __construct($id, array $definition) {
     $this->id = (string) $id;
-    // @todo Split this into proper properties.
+    // @todo Split this into a generic trait.
+    if (isset($definition['version_detector'])) {
+      // @todo Validate the sub-keys.
+      $this->versionDetector = $definition['version_detector'];
+    }
     if (isset($definition['remote_url'])) {
       $this->remoteUrl = $definition['remote_url'];
     }
diff --git a/src/ExternalLibrary/Asset/LocalRemoteAssetTrait.php b/src/ExternalLibrary/Asset/LocalRemoteAssetTrait.php
index dc57c04..ee3e4ac 100644
--- a/src/ExternalLibrary/Asset/LocalRemoteAssetTrait.php
+++ b/src/ExternalLibrary/Asset/LocalRemoteAssetTrait.php
@@ -42,10 +42,6 @@ trait LocalRemoteAssetTrait {
   /**
    * Gets the locator of this library using the locator factory.
    *
-   * Because determining the installation status and library path of a library
-   * is not specific to any library or even any library type, this logic is
-   * offloaded to separate locator objects.
-   *
    * @param \Drupal\Component\Plugin\Factory\FactoryInterface $locator_factory
    *
    * @return \Drupal\libraries\ExternalLibrary\Local\LocatorInterface
diff --git a/src/ExternalLibrary/Asset/SingleAssetLibraryTrait.php b/src/ExternalLibrary/Asset/SingleAssetLibraryTrait.php
index 2dca415..aa890de 100644
--- a/src/ExternalLibrary/Asset/SingleAssetLibraryTrait.php
+++ b/src/ExternalLibrary/Asset/SingleAssetLibraryTrait.php
@@ -17,6 +17,7 @@ use Drupal\libraries\ExternalLibrary\Exception\InvalidLibraryDependencyException
  *
  * @see \Drupal\libraries\ExternalLibrary\Asset\AssetLibraryInterface
  * @see \Drupal\libraries\ExternalLibrary\ExternalLibraryInterface
+ * @see \Drupal\libraries\ExternalLibrary\Version\VersionedLibraryInterface
  */
 trait SingleAssetLibraryTrait {
 
diff --git a/src/ExternalLibrary/Exception/LibraryNotInstalledException.php b/src/ExternalLibrary/Exception/LibraryNotInstalledException.php
index ea44864..5d3bc6a 100644
--- a/src/ExternalLibrary/Exception/LibraryNotInstalledException.php
+++ b/src/ExternalLibrary/Exception/LibraryNotInstalledException.php
@@ -12,7 +12,7 @@ use Drupal\libraries\ExternalLibrary\Utility\LibraryAccessorTrait;
 use Exception;
 
 /**
- * Provides an exception for a library definition that cannot be found.
+ * Provides an exception for a library that is not installed.
  */
 class LibraryNotInstalledException extends \RuntimeException {
 
diff --git a/src/ExternalLibrary/Exception/UnknownLibraryVersionException.php b/src/ExternalLibrary/Exception/UnknownLibraryVersionException.php
new file mode 100644
index 0000000..72bd153
--- /dev/null
+++ b/src/ExternalLibrary/Exception/UnknownLibraryVersionException.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Drupal\libraries\ExternalLibrary\Exception;
+
+use Drupal\libraries\ExternalLibrary\Utility\LibraryAccessorTrait;
+use Drupal\libraries\ExternalLibrary\Version\VersionedLibraryInterface;
+
+/**
+ * Provides an exception for libraries whose version has not been detected.
+ */
+class UnknownLibraryVersionException extends \RuntimeException {
+
+  use LibraryAccessorTrait;
+
+  /**
+   * Constructs a library exception.
+   *
+   * @param \Drupal\libraries\ExternalLibrary\Version\VersionedLibraryInterface $library
+   *   The library.
+   * @param string $message
+   *   (optional) The exception message.
+   * @param int $code
+   *   (optional) The error code.
+   * @param \Exception $previous
+   *   (optional) The previous exception.
+   */
+  public function __construct(
+    VersionedLibraryInterface $library,
+    $message = '',
+    $code = 0,
+    \Exception $previous = NULL
+  ) {
+    $this->library = $library;
+    $message = $message ?: "The version of library '{$this->library->getId()}' could not be detected.";
+    parent::__construct($message, $code, $previous);
+  }
+
+}
diff --git a/src/ExternalLibrary/LibraryInterface.php b/src/ExternalLibrary/LibraryInterface.php
index 024bc23..c3574a5 100644
--- a/src/ExternalLibrary/LibraryInterface.php
+++ b/src/ExternalLibrary/LibraryInterface.php
@@ -23,14 +23,6 @@ interface LibraryInterface {
    */
   public function getId();
 
-  /**
-   * Returns the currently installed version of the library.
-   *
-   * @return string
-   *   The version string, for example 1.0, 2.1.4, or 3.0.0-alpha5.
-   */
-  public function getVersion();
-
   /**
    * Returns the libraries dependencies, if any.
    *
diff --git a/src/ExternalLibrary/LibraryTrait.php b/src/ExternalLibrary/LibraryTrait.php
index f28fd91..836a29d 100644
--- a/src/ExternalLibrary/LibraryTrait.php
+++ b/src/ExternalLibrary/LibraryTrait.php
@@ -16,19 +16,6 @@ trait LibraryTrait {
 
   use IdAccessorTrait;
 
-  /**
-   * Returns the currently installed version of the library.
-   *
-   * @return string
-   *   The version string, for example 1.0, 2.1.4, or 3.0.0-alpha5.
-   *
-   * @see \Drupal\libraries\ExternalLibrary\LibraryInterface::getVersion()
-   */
-  public function getVersion() {
-    // @todo Turn into something useful and split into some other trait.
-    return '1.0';
-  }
-
   /**
    * Returns the libraries dependencies, if any.
    *
diff --git a/src/ExternalLibrary/Version/VersionDetectorInterface.php b/src/ExternalLibrary/Version/VersionDetectorInterface.php
new file mode 100644
index 0000000..da42b33
--- /dev/null
+++ b/src/ExternalLibrary/Version/VersionDetectorInterface.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Drupal\libraries\ExternalLibrary\Version;
+
+/**
+ * Provides an interface for version detectors.
+ */
+interface VersionDetectorInterface {
+
+  /**
+   * Detects the version of a library.
+   *
+   * @param \Drupal\libraries\ExternalLibrary\Version\VersionedLibraryInterface $library
+   *   The library whose version to detect.
+   *
+   * @throws \Drupal\libraries\ExternalLibrary\Exception\UnknownLibraryVersionException
+   *
+   * @todo Provide a mechanism for version detectors to provide a reason for
+   *   failing.
+   */
+  public function detectVersion(VersionedLibraryInterface $library);
+
+}
diff --git a/src/ExternalLibrary/Version/VersionDetectorManager.php b/src/ExternalLibrary/Version/VersionDetectorManager.php
new file mode 100644
index 0000000..d7160b0
--- /dev/null
+++ b/src/ExternalLibrary/Version/VersionDetectorManager.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Drupal\libraries\ExternalLibrary\Version;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\libraries\Annotation\VersionDetector;
+use Drupal\libraries\ExternalLibrary\Version\VersionDetectorInterface;
+
+/**
+ * Provides a plugin manager for library version detector plugins.
+ *
+ * @see \Drupal\libraries\ExternalLibrary\Version\VersionDetectorInterface
+ */
+class VersionDetectorManager extends DefaultPluginManager {
+
+  /**
+   * Constructs a version detector manager.
+   *
+   * @param \Traversable $namespaces
+   *   An object that implements \Traversable which contains the root paths
+   *   keyed by the corresponding namespace to look for plugin implementations.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+   *   Cache backend instance to use.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler to invoke the alter hook with.
+   */
+  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
+    parent::__construct('Plugin/libraries/VersionDetector', $namespaces, $module_handler, VersionDetectorInterface::class, VersionDetector::class);
+    // @todo Document this hook.
+    $this->alterInfo('libraries_version_detector_info');
+    $this->setCacheBackend($cache_backend, 'libraries_version_detector_info');
+  }
+
+}
diff --git a/src/ExternalLibrary/Version/VersionedLibraryInterface.php b/src/ExternalLibrary/Version/VersionedLibraryInterface.php
new file mode 100644
index 0000000..d9b8df4
--- /dev/null
+++ b/src/ExternalLibrary/Version/VersionedLibraryInterface.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace Drupal\libraries\ExternalLibrary\Version;
+
+use Drupal\Component\Plugin\Factory\FactoryInterface;
+use Drupal\libraries\ExternalLibrary\LibraryInterface;
+
+/**
+ * Provides an interface for versioned libraries.
+ *
+ * Version detection and negotiation is a key aspect of Libraries API's
+ * functionality so every type of library should implement this interface.
+ */
+interface VersionedLibraryInterface extends LibraryInterface {
+
+  /**
+   * Gets the version of the library.
+   *
+   * @return string
+   *   The version string, for example 1.0, 2.1.4, or 3.0.0-alpha5.
+   *
+   * @throws \Drupal\libraries\ExternalLibrary\Exception\UnknownLibraryVersionException
+   *
+   * @see \Drupal\libraries\ExternalLibrary\Version\VersionedLibraryInterface::setVersion()
+   */
+  public function getVersion();
+
+  /**
+   * Sets the version of the library.
+   *
+   * @param string $version
+   *   The version of the library.
+   *
+   * @reutrn $this
+   *
+   * @see \Drupal\libraries\ExternalLibrary\Version\VersionedLibraryInterface::getVersion()
+   */
+  public function setVersion($version);
+
+  /**
+   * Gets the version detector of this library using the detector factory.
+   *
+   * Because determining the installation version of a library is not specific
+   * to any library or even any library type, this logic is offloaded to
+   * separate detector objects.
+   *
+   * @param \Drupal\Component\Plugin\Factory\FactoryInterface $detector_factory
+   *
+   * @return \Drupal\libraries\ExternalLibrary\Version\VersionDetectorInterface
+   *
+   * @see \Drupal\libraries\ExternalLibrary\Version\VersionDetectorInterface
+   */
+  public function getVersionDetector(FactoryInterface $detector_factory);
+
+}
diff --git a/src/ExternalLibrary/Version/VersionedLibraryTrait.php b/src/ExternalLibrary/Version/VersionedLibraryTrait.php
new file mode 100644
index 0000000..2404a10
--- /dev/null
+++ b/src/ExternalLibrary/Version/VersionedLibraryTrait.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Drupal\libraries\ExternalLibrary\Version;
+
+use Drupal\Component\Plugin\Factory\FactoryInterface;
+use Drupal\libraries\ExternalLibrary\Exception\UnknownLibraryVersionException;
+
+/**
+ * Provides a trait for versioned libraries.
+ *
+ * @see \Drupal\libraries\ExternalLibrary\Version\VersionedLibraryInterface
+ */
+trait VersionedLibraryTrait {
+
+  /**
+   * The library version.
+   *
+   * @var string
+   */
+  protected $version;
+
+  /**
+   * Information about the version detector to use fo rthis library.
+   *
+   * Contains the following keys:
+   * id: The plugin ID of the version detector.
+   * configuration: The plugin configuration of the version detector.
+   *
+   * @var array
+   */
+  protected $versionDetector = [
+    'id' => NULL,
+    'configuration' => [],
+  ];
+
+  /**
+   * Gets the version of the library.
+   *
+   * @return string
+   *   The version string, for example 1.0, 2.1.4, or 3.0.0-alpha5.
+   *
+   * @throws \Drupal\libraries\ExternalLibrary\Exception\UnknownLibraryVersionException
+   *
+   * @see \Drupal\libraries\ExternalLibrary\Version\VersionedLibraryInterface::getVersion()
+   */
+  public function getVersion() {
+    if (!isset($this->version)) {
+      throw new UnknownLibraryVersionException($this);
+    }
+    return $this->version;
+  }
+
+  /**
+   * Sets the version of the library.
+   *
+   * @param string $version
+   *   The version of the library.
+   *
+   * @return $this
+   *
+   * @see \Drupal\libraries\ExternalLibrary\Version\VersionedLibraryInterface::setVersion()
+   */
+  public function setVersion($version) {
+    $this->version = (string) $version;
+    return $this;
+  }
+
+  /**
+   * Gets the version detector of this library using the detector factory.
+   *
+   * Because determining the installation version of a library is not specific
+   * to any library or even any library type, this logic is offloaded to
+   * separate detector objects.
+   *
+   * @param \Drupal\Component\Plugin\Factory\FactoryInterface $detector_factory
+   *
+   * @return \Drupal\libraries\ExternalLibrary\Version\VersionDetectorInterface
+   *
+   * @see \Drupal\libraries\ExternalLibrary\Version\VersionDetectorInterface
+   */
+  public function getVersionDetector(FactoryInterface $detector_factory) {
+    return $detector_factory->createInstance($this->versionDetector['id'], $this->versionDetector['configuration']);
+  }
+
+}
diff --git a/src/Plugin/libraries/LibraryType/AssetLibraryType.php b/src/Plugin/libraries/LibraryType/AssetLibraryType.php
index 9c1b262..ac5c3c9 100644
--- a/src/Plugin/libraries/LibraryType/AssetLibraryType.php
+++ b/src/Plugin/libraries/LibraryType/AssetLibraryType.php
@@ -15,6 +15,7 @@ use Drupal\libraries\ExternalLibrary\LibraryType\LibraryCreationListenerInterfac
 use Drupal\libraries\ExternalLibrary\LibraryType\LibraryTypeInterface;
 use Drupal\libraries\ExternalLibrary\Local\LocalLibraryInterface;
 use Drupal\libraries\ExternalLibrary\Utility\IdAccessorTrait;
+use Drupal\libraries\ExternalLibrary\Version\VersionedLibraryInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -35,6 +36,13 @@ class AssetLibraryType implements
    */
   protected $locatorFactory;
 
+  /**
+   * The version detector factory.
+   *
+   * @var \Drupal\Component\Plugin\Factory\FactoryInterface
+   */
+  protected $detectorFactory;
+
   /**
    * Constructs the asset library type.
    *
@@ -42,17 +50,24 @@ class AssetLibraryType implements
    *   The plugin ID taken from the class annotation.
    * @param \Drupal\Component\Plugin\Factory\FactoryInterface $locator_factory
    *   The locator factory.
+   * @param \Drupal\Component\Plugin\Factory\FactoryInterface $detector_factory
+   *   The version detector factory.
    */
-  public function __construct($plugin_id, FactoryInterface $locator_factory) {
+  public function __construct($plugin_id, FactoryInterface $locator_factory, FactoryInterface $detector_factory) {
     $this->id = $plugin_id;
     $this->locatorFactory = $locator_factory;
+    $this->detectorFactory = $detector_factory;
   }
 
   /**
    * {@inheritdoc}
    */
   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
-    return new static($plugin_id, $container->get('plugin.manager.libraries.locator'));
+    return new static(
+      $plugin_id,
+      $container->get('plugin.manager.libraries.locator'),
+      $container->get('plugin.manager.libraries.version_detector')
+    );
   }
 
   /**
@@ -71,6 +86,9 @@ class AssetLibraryType implements
     if ($library instanceof LocalLibraryInterface) {
       $library->getLocator($this->locatorFactory)->locate($library);
     }
+    if ($library instanceof VersionedLibraryInterface) {
+      $library->getVersionDetector($this->detectorFactory)->detectVersion($library);
+    }
   }
 
 }
diff --git a/src/Plugin/libraries/LibraryType/PhpFileLibraryType.php b/src/Plugin/libraries/LibraryType/PhpFileLibraryType.php
index dced4c3..821bf6a 100644
--- a/src/Plugin/libraries/LibraryType/PhpFileLibraryType.php
+++ b/src/Plugin/libraries/LibraryType/PhpFileLibraryType.php
@@ -16,6 +16,7 @@ use Drupal\libraries\ExternalLibrary\LibraryType\LibraryTypeInterface;
 use Drupal\libraries\ExternalLibrary\PhpFile\PhpFileLibrary;
 use Drupal\libraries\ExternalLibrary\PhpFile\PhpFileLoaderInterface;
 use Drupal\libraries\ExternalLibrary\Utility\IdAccessorTrait;
+use Drupal\libraries\ExternalLibrary\Version\VersionedLibraryInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -37,6 +38,13 @@ class PhpFileLibraryType implements
    */
   protected $locatorFactory;
 
+  /**
+   * The version detector factory.
+   *
+   * @var \Drupal\Component\Plugin\Factory\FactoryInterface
+   */
+  protected $detectorFactory;
+
   /**
    * The PHP file loader.
    *
@@ -51,12 +59,15 @@ class PhpFileLibraryType implements
    *   The plugin ID taken from the class annotation.
    * @param \Drupal\Component\Plugin\Factory\FactoryInterface $locator_factory
    *   The locator factory.
+   * @param \Drupal\Component\Plugin\Factory\FactoryInterface $detector_factory
+   *   The version detector factory.
    * @param \Drupal\libraries\ExternalLibrary\PhpFile\PhpFileLoaderInterface $php_file_loader
    *   The PHP file loader.
    */
-  public function __construct($plugin_id, FactoryInterface $locator_factory, PhpFileLoaderInterface $php_file_loader) {
+  public function __construct($plugin_id, FactoryInterface $locator_factory, FactoryInterface $detector_factory, PhpFileLoaderInterface $php_file_loader) {
     $this->id = $plugin_id;
     $this->locatorFactory = $locator_factory;
+    $this->detectorFactory = $detector_factory;
     $this->phpFileLoader = $php_file_loader;
   }
 
@@ -67,6 +78,7 @@ class PhpFileLibraryType implements
     return new static(
       $plugin_id,
       $container->get('plugin.manager.libraries.locator'),
+      $container->get('plugin.manager.libraries.version_detector'),
       $container->get('libraries.php_file_loader')
     );
   }
@@ -82,8 +94,11 @@ class PhpFileLibraryType implements
    * {@inheritdoc}
    */
   public function onLibraryCreate(LibraryInterface $library) {
-    /** @var \Drupal\libraries\ExternalLibrary\PhpFile\PhpFileLibraryInterface $library */
+    /** @var \Drupal\libraries\ExternalLibrary\PhpFile\PhpFileLibraryInterface|\Drupal\libraries\ExternalLibrary\Version\VersionedLibraryInterface $library */
     $library->getLocator($this->locatorFactory)->locate($library);
+    if ($library instanceof VersionedLibraryInterface) {
+      $library->getVersionDetector($this->detectorFactory)->detectVersion($library);
+    }
   }
 
   /**
@@ -94,6 +109,7 @@ class PhpFileLibraryType implements
     foreach ($library->getPhpFiles() as $file) {
       $this->phpFileLoader->load($file);
     }
+
   }
 
 }
diff --git a/src/Plugin/libraries/VersionDetector/LinePatternDetector.php b/src/Plugin/libraries/VersionDetector/LinePatternDetector.php
new file mode 100644
index 0000000..9a8ece7
--- /dev/null
+++ b/src/Plugin/libraries/VersionDetector/LinePatternDetector.php
@@ -0,0 +1,86 @@
+<?php
+
+namespace Drupal\libraries\Plugin\libraries\VersionDetector;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Plugin\PluginBase;
+use Drupal\libraries\ExternalLibrary\Exception\UnknownLibraryVersionException;
+use Drupal\libraries\ExternalLibrary\Local\LocalLibraryInterface;
+use Drupal\libraries\ExternalLibrary\Version\VersionDetectorInterface;
+use Drupal\libraries\ExternalLibrary\Version\VersionedLibraryInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Detects the version by matching lines in a file against a specified pattern.
+ *
+ * This version detector can be used if the library version is denoted in a
+ * particular format in a changelog or readme file, for example.
+ *
+ * @VersionDetector("line_pattern")
+ */
+class LinePatternDetector extends PluginBase implements VersionDetectorInterface, ContainerFactoryPluginInterface {
+
+  /**
+   * The app root.
+   *
+   * @var string
+   */
+  protected $appRoot;
+
+  /**
+   * Constructs a line pattern version detector.
+   *
+   * @param array $configuration
+   * @param string $plugin_id
+   * @param array $plugin_definition
+   * @param string $app_root
+   */
+  public function __construct(array $configuration, $plugin_id, array $plugin_definition, $app_root) {
+    $configuration += [
+      'file' => '',
+      'pattern' => '',
+      'lines' => 20,
+      'columns' => 200,
+    ];
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->appRoot = $app_root;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('app.root')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function detectVersion(VersionedLibraryInterface $library) {
+    if (!($library instanceof LocalLibraryInterface)) {
+      throw new UnknownLibraryVersionException($library);
+    }
+
+    $filepath = $this->appRoot . '/' . $library->getLocalPath() . '/' . $this->configuration['file'];
+    if (!file_exists($filepath)) {
+      throw new UnknownLibraryVersionException($library);
+    }
+
+    $file = fopen($filepath, 'r');
+    $lines = $this->configuration['lines'];
+    while ($lines && $line = fgets($file, $this->configuration['columns'])) {
+      if (preg_match($this->configuration['pattern'], $line, $version)) {
+        fclose($file);
+        $library->setVersion($version[1]);
+        return;
+      }
+      $lines--;
+    }
+    fclose($file);
+  }
+
+}
diff --git a/src/Plugin/libraries/VersionDetector/StaticDetector.php b/src/Plugin/libraries/VersionDetector/StaticDetector.php
new file mode 100644
index 0000000..2a4258d
--- /dev/null
+++ b/src/Plugin/libraries/VersionDetector/StaticDetector.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Drupal\libraries\Plugin\libraries\VersionDetector;
+use Drupal\Core\Plugin\PluginBase;
+use Drupal\libraries\ExternalLibrary\Exception\UnknownLibraryVersionException;
+use Drupal\libraries\ExternalLibrary\Version\VersionDetectorInterface;
+use Drupal\libraries\ExternalLibrary\Version\VersionedLibraryInterface;
+
+/**
+ * Detects the version by returning a static string.
+ *
+ * As this does not perform any actual detection and, thus, circumvents any
+ * negotiation of versions by Libraries API it should only be used for testing
+ * or when the version of a library cannot be determined from the source code
+ * itself.
+ *
+ * @VersionDetector("static")
+ */
+class StaticDetector extends PluginBase implements VersionDetectorInterface {
+
+  /**
+   * Constructs a static version detector.
+   *
+   * @param array $configuration
+   * @param string $plugin_id
+   * @param array $plugin_definition
+   */
+  public function __construct(array $configuration, $plugin_id, array $plugin_definition) {
+    $configuration += [
+      'version' => NULL,
+    ];
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function detectVersion(VersionedLibraryInterface $library) {
+    if (!isset($this->configuration['version'])) {
+      throw new UnknownLibraryVersionException($library);
+    }
+    $library->setVersion($this->configuration['version']);
+  }
+
+}
diff --git a/tests/library_definitions/test_asset_library.yml b/tests/library_definitions/test_asset_library.yml
index 3c6e1a1..4c712e6 100644
--- a/tests/library_definitions/test_asset_library.yml
+++ b/tests/library_definitions/test_asset_library.yml
@@ -1,4 +1,8 @@
 type: asset
+version_detector:
+  id: static
+  configuration:
+    version: '1.0.0'
 remote_url: http://example.com
 css:
   base:
diff --git a/tests/src/Kernel/ExternalLibrary/Asset/AssetLibraryTest.php b/tests/src/Kernel/ExternalLibrary/Asset/AssetLibraryTest.php
index 851aa3a..9b840d4 100644
--- a/tests/src/Kernel/ExternalLibrary/Asset/AssetLibraryTest.php
+++ b/tests/src/Kernel/ExternalLibrary/Asset/AssetLibraryTest.php
@@ -1,10 +1,5 @@
 <?php
 
-/**
- * @file
- * Contains \Drupal\Tests\libraries\Kernel\ExternalLibrary\Asset\AssetLibraryTest.
- */
-
 namespace Drupal\Tests\libraries\Kernel\ExternalLibrary\Asset;
 
 use Drupal\libraries\ExternalLibrary\Asset\AssetLibrary;
@@ -55,7 +50,7 @@ class AssetLibraryTest extends LibraryKernelTestBase {
 
       $this->assertEquals('test_asset_library', $library->getId());
       $expected = ['test_asset_library' => [
-        'version' => 1.0,
+        'version' => '1.0.0',
         'css' => ['base' => ['http://example.com/example.css' => []]],
         'js' => ['http://example.com/example.js' => []],
         'dependencies' => [],
@@ -84,19 +79,19 @@ class AssetLibraryTest extends LibraryKernelTestBase {
   public function testAssetLibraryRemote() {
     $library = $this->libraryDiscovery->getLibraryByName('libraries', 'test_asset_library');
     $expected = [
-      'version' => '1.0',
+      'version' => '1.0.0',
       'css' => [[
         'weight' => -200,
         'group' => 0,
         'type' => 'external',
         'data' => 'http://example.com/example.css',
-        'version' => '1.0',
+        'version' => '1.0.0',
       ]],
       'js' => [[
         'group' => -100,
         'type' => 'external',
         'data' => 'http://example.com/example.js',
-        'version' => '1.0',
+        'version' => '1.0.0',
       ]],
       'dependencies' => [],
       'license' => [
@@ -120,19 +115,19 @@ class AssetLibraryTest extends LibraryKernelTestBase {
     $this->libraryDiscovery->clearCachedDefinitions();
     $library = $this->libraryDiscovery->getLibraryByName('libraries', 'test_asset_library');
     $expected = [
-      'version' => '1.0',
+      'version' => '1.0.0',
       'css' => [[
         'weight' => -200,
         'group' => 0,
         'type' => 'file',
         'data' => $this->modulePath . '/tests/assets/vendor/test_asset_library/example.css',
-        'version' => '1.0',
+        'version' => '1.0.0',
       ]],
       'js' => [[
         'group' => -100,
         'type' => 'file',
         'data' => $this->modulePath . '/tests/assets/vendor/test_asset_library/example.js',
-        'version' => '1.0',
+        'version' => '1.0.0',
         'minified' => FALSE,
       ]],
       'dependencies' => [],
diff --git a/tests/src/Kernel/ExternalLibrary/PhpFile/PhpFileLibraryTest.php b/tests/src/Kernel/ExternalLibrary/PhpFile/PhpFileLibraryTest.php
index 7897472..9e5d1e8 100644
--- a/tests/src/Kernel/ExternalLibrary/PhpFile/PhpFileLibraryTest.php
+++ b/tests/src/Kernel/ExternalLibrary/PhpFile/PhpFileLibraryTest.php
@@ -1,10 +1,5 @@
 <?php
 
-/**
- * @file
- * Contains \Drupal\Tests\libraries\Kernel\ExternalLibrary\PhpFile\PhpFileLibraryTest.
- */
-
 namespace Drupal\Tests\libraries\Kernel\ExternalLibrary\PhpFile;
 
 use Drupal\libraries\ExternalLibrary\Exception\LibraryClassNotFoundException;
diff --git a/tests/src/Kernel/ExternalLibrary/TestLibraryFilesStream.php b/tests/src/Kernel/ExternalLibrary/TestLibraryFilesStream.php
index c18faea..fd29e92 100644
--- a/tests/src/Kernel/ExternalLibrary/TestLibraryFilesStream.php
+++ b/tests/src/Kernel/ExternalLibrary/TestLibraryFilesStream.php
@@ -1,10 +1,5 @@
 <?php
 
-/**
- * @file
- * Contains \Drupal\Tests\libraries\Kernel\ExternalLibrary\TestPhpLibraryFilesStream.
- */
-
 namespace Drupal\Tests\libraries\Kernel\ExternalLibrary;
 
 use Drupal\Core\Extension\ModuleHandlerInterface;
diff --git a/tests/src/Kernel/LibraryKernelTestBase.php b/tests/src/Kernel/LibraryKernelTestBase.php
index 32e5f85..6b5cc96 100644
--- a/tests/src/Kernel/LibraryKernelTestBase.php
+++ b/tests/src/Kernel/LibraryKernelTestBase.php
@@ -1,10 +1,5 @@
 <?php
 
-/**
- * @file
- * Contains \Drupal\Tests\libraries\Kernel\LibraryKernelTestBase.
- */
-
 namespace Drupal\Tests\libraries\Kernel;
 
 use Drupal\KernelTests\KernelTestBase;
diff --git a/tests/src/Unit/Plugin/libraries/VersionDetector/LinePatternDetectorTest.php b/tests/src/Unit/Plugin/libraries/VersionDetector/LinePatternDetectorTest.php
new file mode 100644
index 0000000..0d1cdd3
--- /dev/null
+++ b/tests/src/Unit/Plugin/libraries/VersionDetector/LinePatternDetectorTest.php
@@ -0,0 +1,195 @@
+<?php
+
+namespace Drupal\Tests\libraries\Unit\Plugin\libraries\VersionDetector;
+use Drupal\libraries\ExternalLibrary\Exception\UnknownLibraryVersionException;
+use Drupal\libraries\ExternalLibrary\Local\LocalLibraryInterface;
+use Drupal\libraries\ExternalLibrary\Version\VersionedLibraryInterface;
+use Drupal\libraries\Plugin\libraries\VersionDetector\LinePatternDetector;
+use Drupal\Tests\UnitTestCase;
+use org\bovigo\vfs\vfsStream;
+
+/**
+ * Tests the line pattern version detector.
+ *
+ * @coversDefaultClass \Drupal\libraries\Plugin\libraries\VersionDetector\LinePatternDetector
+ */
+class LinePatternDetectorTest extends UnitTestCase {
+
+  protected $libraryId = 'test_library';
+
+  /**
+   * Tests that version detection fails for a non-local library.
+   *
+   * @expectedException \Drupal\libraries\ExternalLibrary\Exception\UnknownLibraryVersionException
+   *
+   * @covers ::detectVersion
+   */
+  public function testDetectVersionNonLocal() {
+    $library = $this->prophesize(VersionedLibraryInterface::class);
+    $detector = $this->setupDetector();
+    $detector->detectVersion($library->reveal());
+  }
+
+  /**
+   * Tests that version detection fails for a missing file.
+   *
+   * @expectedException \Drupal\libraries\ExternalLibrary\Exception\UnknownLibraryVersionException
+   *
+   * @covers ::detectVersion
+   */
+  public function testDetectVersionMissingFile() {
+    $library = $this->setupLibrary();
+
+    $detector = $this->setupDetector(['file' => 'CHANGELOG.txt']);
+    $detector->detectVersion($library->reveal());
+  }
+
+  /**
+   * Tests that version detection fails without a version in the file.
+   *
+   * @dataProvider providerTestDetectVersionNoVersion
+   *
+   * @covers ::detectVersion
+   */
+  public function testDetectVersionNoVersion($configuration, $file_contents) {
+    $library = $this->setupLibrary();
+
+    $detector = $this->setupDetector($configuration);
+    $this->setupFile($configuration['file'], $file_contents);
+
+    $library->setVersion()->shouldNotBeCalled();
+    $detector->detectVersion($library->reveal());
+  }
+
+  /**
+   * @return array
+   */
+  public function providerTestDetectVersionNoVersion() {
+    $test_cases = [];
+
+    $configuration = [
+      'file' => 'CHANGELOG.txt',
+      'pattern' => '/@version (\d+\.\d+\.\d+)/'
+    ];
+
+    $test_cases['empty_file'] = [$configuration, ''];
+
+    $test_cases['no_version'] = [$configuration, <<<EOF
+This is a file with
+multiple lines that does
+not contain a version.
+EOF
+    ];
+
+    $configuration['lines'] = 3;
+    $test_cases['long_file'] = [$configuration, <<<EOF
+This is a file that
+contains the version after
+the maximum number of lines
+to test has been surpassed.
+
+@version 1.2.3
+EOF
+    ];
+
+    $configuration['columns'] = 10;
+    // @todo Document why this is necessary.
+    $configuration['lines'] = 2;
+    $test_cases['long_column'] = [$configuration, <<<EOF
+This is a file that contains the version after
+the maximum number of columns to test has been surpassed. @version 1.2.3
+EOF
+    ];
+
+    return $test_cases;
+  }
+
+  /**
+   * Tests that version detection succeeds with a version in the file.
+   *
+   * @dataProvider providerTestDetectVersion
+   *
+   * @covers ::detectVersion
+   */
+  public function testDetectVersion($configuration, $file_contents, $version) {
+    $library = $this->setupLibrary();
+
+    $detector = $this->setupDetector($configuration);
+    $this->setupFile($configuration['file'], $file_contents);
+
+    $library->setVersion($version)->shouldBeCalled();
+    $detector->detectVersion($library->reveal());
+  }
+
+  /**
+   * @return array
+   */
+  public function providerTestDetectVersion() {
+    $test_cases = [];
+
+    $configuration = [
+      'file' => 'CHANGELOG.txt',
+      'pattern' => '/@version (\d+\.\d+\.\d+)/'
+    ];
+    $version = '1.2.3';
+
+    $test_cases['version'] = [$configuration, <<<EOF
+This a file with a version
+
+@version $version
+EOF
+    , $version];
+
+    return $test_cases;
+  }
+
+  /**
+   * Sets up the library prophecy and returns it.
+   *
+   * @return \Prophecy\Prophecy\ObjectProphecy
+   */
+  protected function setupLibrary() {
+    $library = $this->prophesize(VersionedLibraryInterface::class);
+    $library->willImplement(LocalLibraryInterface::class);
+    $library->getId()->willReturn($this->libraryId);
+    $library->getLocalPath()->willReturn('libraries/' . $this->libraryId);
+    return $library;
+  }
+
+  /**
+   * Sets up the version detector for testing and returns it.
+   *
+   * @param array $configuration
+   *   The plugin configuration to set the version detector up with.
+   *
+   * @return \Drupal\libraries\Plugin\libraries\VersionDetector\LinePatternDetector
+   *   The line pattern version detector to test.
+   */
+  protected function setupDetector(array $configuration = []) {
+    $app_root = 'root';
+    vfsStream::setup($app_root);
+
+    $plugin_id = 'line_pattern';
+    $plugin_definition = [
+      'id' => $plugin_id,
+      'class' => LinePatternDetector::class,
+      'provider' => 'libraries',
+    ];
+    return new LinePatternDetector($configuration, $plugin_id, $plugin_definition, 'vfs://' . $app_root);
+  }
+
+  /**
+   * @param $file
+   * @param $file_contents
+   */
+  protected function setupFile($file, $file_contents) {
+    vfsStream::create([
+      'libraries' => [
+        $this->libraryId => [
+          $file => $file_contents,
+        ],
+      ],
+    ]);
+  }
+
+}
-- 
GitLab