From f166d464aced0618119b20cb9ab4548d9f0eaab3 Mon Sep 17 00:00:00 2001
From: Achilles Katsaros <achilleas01@yahoo.gr>
Date: Wed, 27 Sep 2017 10:41:55 +0300
Subject: [PATCH] add drupal modules files

---
 drupal/modules/README.txt                     |  42 +
 drupal/modules/contrib/consumers              |   1 +
 .../modules/contrib/simple_oauth/.travis.yml  |  67 ++
 .../modules/contrib/simple_oauth/LICENSE.txt  | 339 +++++++
 drupal/modules/contrib/simple_oauth/README.md |  59 ++
 .../contrib/simple_oauth/composer.json        |  15 +
 ...oauth.oauth2_token.bundle.access_token.yml |   6 +
 ...le_oauth.oauth2_token.bundle.auth_code.yml |   6 +
 ...auth.oauth2_token.bundle.refresh_token.yml |   6 +
 .../config/install/simple_oauth.settings.yml  |   2 +
 .../config/schema/simple_oauth.schema.yml     |  37 +
 .../simple_oauth/simple_oauth.info.yml        |  16 +
 .../contrib/simple_oauth/simple_oauth.install |  13 +
 .../simple_oauth.links.action.yml             |   5 +
 .../simple_oauth/simple_oauth.links.menu.yml  |   5 +
 .../simple_oauth/simple_oauth.links.task.yml  |  27 +
 .../contrib/simple_oauth/simple_oauth.module  | 162 ++++
 .../simple_oauth/simple_oauth.permissions.yml |  15 +
 .../simple_oauth/simple_oauth.routing.yml     |  50 +
 .../simple_oauth/simple_oauth.services.yml    |  59 ++
 .../install/simple_oauth_extras.settings.yml  |   1 +
 .../schema/simple_oauth_extras.schema.yml     |   8 +
 .../simple_oauth_extras.info.yml              |  13 +
 .../simple_oauth_extras.module                | 104 +++
 .../simple_oauth_extras.permissions.yml       |   3 +
 .../simple_oauth_extras.routing.yml           |  22 +
 .../simple_oauth_extras.services.yml          |   4 +
 .../src/Controller/DebugController.php        |  62 ++
 .../src/Controller/Oauth2AuthorizeForm.php    | 218 +++++
 .../src/Entities/AuthCodeEntity.php           |  14 +
 .../Plugin/Oauth2Grant/AuthorizationCode.php  |  73 ++
 .../Plugin/Oauth2Grant/ClientCredentials.php  |  25 +
 .../src/Plugin/Oauth2Grant/Code.php           |  12 +
 .../src/Plugin/Oauth2Grant/Implicit.php       |  58 ++
 .../src/Plugin/Oauth2Grant/RefreshToken.php   |  64 ++
 .../src/Repositories/AuthCodeRepository.php   |  45 +
 .../simple_oauth_extras_test.info.yml         |  14 +
 .../simple_oauth_extras_test.routing.yml      |   8 +
 .../src/Controller/RedirectDebug.php          |  18 +
 .../src/Functional/AuthCodeFunctionalTest.php | 102 ++
 .../ClientCredentialsFunctionalTest.php       | 117 +++
 .../src/Functional/ImplicitFunctionalTest.php |  92 ++
 .../src/Functional/RefreshFunctionalTest.php  | 179 ++++
 .../RolesNegotiationFunctionalTest.php        | 320 +++++++
 .../src/AccessTokenAccessControlHandler.php   |  16 +
 .../src/Annotation/Oauth2Grant.php            |  33 +
 .../SimpleOauthAuthenticationProvider.php     |  79 ++
 ...leOauthAuthenticationProviderInterface.php |  25 +
 .../src/Authentication/TokenAuthUser.php      | 865 +++++++++++++++++
 .../Authentication/TokenAuthUserInterface.php |  17 +
 .../src/Controller/Oauth2Token.php            |  70 ++
 .../src/Entities/AccessTokenEntity.php        |  14 +
 .../src/Entities/ClientEntity.php             |  47 +
 .../src/Entities/ClientEntityInterface.php    |  26 +
 .../src/Entities/RefreshTokenEntity.php       |  13 +
 .../simple_oauth/src/Entities/ScopeEntity.php |  29 +
 .../simple_oauth/src/Entities/UserEntity.php  |  12 +
 .../Entity/ConfigEntityLockableInterface.php  |  26 +
 .../src/Entity/ConfigEntityLockableTrait.php  |  37 +
 .../src/Entity/Form/Oauth2TokenDeleteForm.php |  55 ++
 .../Entity/Form/Oauth2TokenSettingsForm.php   | 115 +++
 .../simple_oauth/src/Entity/Oauth2Token.php   | 224 +++++
 .../src/Entity/Oauth2TokenInterface.php       |  28 +
 .../src/Entity/Oauth2TokenType.php            |  67 ++
 .../src/Entity/Oauth2TokenTypeInterface.php   |  26 +
 .../simple_oauth/src/ExpiredCollector.php     | 131 +++
 .../src/HttpMiddleware/BasicAuthSwap.php      |  70 ++
 ...ckableConfigEntityAccessControlHandler.php |  26 +
 .../RefreshTokenEntityNormalizer.php          |  28 +
 .../src/Normalizer/TokenEntityNormalizer.php  |  56 ++
 .../TokenEntityNormalizerInterface.php        |   7 +
 .../src/Oauth2TokenListBuilder.php            |  57 ++
 .../PageCache/DisallowSimpleOauthRequests.php |  22 +
 .../src/Plugin/Oauth2Grant/Password.php       |  72 ++
 .../src/Plugin/Oauth2GrantBase.php            |  24 +
 .../src/Plugin/Oauth2GrantInterface.php       |  21 +
 .../src/Plugin/Oauth2GrantManager.php         | 146 +++
 .../Plugin/Oauth2GrantManagerInterface.php    |  22 +
 .../Repositories/AccessTokenRepository.php    |  53 ++
 .../src/Repositories/ClientRepository.php     |  56 ++
 .../Repositories/RefreshTokenRepository.php   |  44 +
 .../RevocableTokenRepositoryTrait.php         |  87 ++
 .../src/Repositories/ScopeRepository.php      | 123 +++
 .../src/Repositories/UserRepository.php       |  42 +
 .../src/Server/ResourceServer.php             |  69 ++
 .../src/Server/ResourceServerInterface.php    |  23 +
 .../tests/certificates/private.key            |  27 +
 .../tests/certificates/public.key             |   9 +
 .../src/Functional/PasswordFunctionalTest.php | 151 +++
 .../src/Functional/RequestHelperTrait.php     |  47 +
 .../TokenBearerFunctionalTestBase.php         | 178 ++++
 .../SimpleOauthAuthenticationTest.php         |  76 ++
 .../tests/src/Unit/EntityCollectorTest.php    | 117 +++
 .../datacenter/admin_area_entity.page.inc     |  30 +
 .../modules/custom/datacenter/composer.json   |  14 +
 .../install/views.view.frontistiria.yml       | 849 +++++++++++++++++
 .../config/install/views.view.get_areas.yml   | 252 +++++
 .../custom/datacenter/datacenter.info.yml     |   7 +
 .../datacenter/datacenter.links.action.yml    |  10 +
 .../datacenter/datacenter.links.menu.yml      |  27 +
 .../datacenter/datacenter.links.task.yml      |  44 +
 .../custom/datacenter/datacenter.module       |  35 +
 .../datacenter/datacenter.permissions.yml     |  39 +
 .../datacenter/licenses_entity.page.inc       |  30 +
 .../AdminAreaEntityAccessControlHandler.php   |  47 +
 .../src/AdminAreaEntityHtmlRouteProvider.php  |  85 ++
 .../src/AdminAreaEntityListBuilder.php        |  40 +
 .../datacenter/src/Entity/AdminAreaEntity.php | 267 ++++++
 .../src/Entity/AdminAreaEntityInterface.php   |  77 ++
 .../src/Entity/AdminAreaEntityViewsData.php   |  24 +
 .../datacenter/src/Entity/LicensesEntity.php  | 284 ++++++
 .../src/Entity/LicensesEntityInterface.php    |  70 ++
 .../src/Entity/LicensesEntityViewsData.php    |  24 +
 .../src/Form/AdminAreaEntityDeleteForm.php    |  15 +
 .../src/Form/AdminAreaEntityForm.php          |  50 +
 .../src/Form/AdminAreaEntitySettingsForm.php  |  55 ++
 .../src/Form/LicensesEntityDeleteForm.php     |  15 +
 .../src/Form/LicensesEntityForm.php           |  50 +
 .../src/Form/LicensesEntitySettingsForm.php   |  55 ++
 .../LicensesEntityAccessControlHandler.php    |  47 +
 .../src/LicensesEntityHtmlRouteProvider.php   |  85 ++
 .../src/LicensesEntityListBuilder.php         |  40 +
 .../templates/admin_area_entity.html.twig     |  22 +
 .../datacenter/templates/datacenter.html.twig |   1 +
 .../templates/licenses_entity.html.twig       |  22 +
 .../modules/custom/datachanger/composer.json  |  14 +
 .../custom/datachanger/datachanger.info.yml   |   9 +
 .../custom/datachanger/datachanger.module     | 184 ++++
 .../datachanger/datachanger.routing.yml       |  47 +
 .../src/Controller/DataController.php         | 675 ++++++++++++++
 .../templates/datachanger.html.twig           |   1 +
 drupal/modules/jsonapi/LICENSE.txt            | 339 +++++++
 drupal/modules/jsonapi/README.md              |  46 +
 drupal/modules/jsonapi/composer.json          |   9 +
 drupal/modules/jsonapi/jsonapi.info.yml       |  14 +
 drupal/modules/jsonapi/jsonapi.module         |  57 ++
 .../modules/jsonapi/jsonapi.permissions.yml   |   3 +
 drupal/modules/jsonapi/jsonapi.routing.yml    |   3 +
 drupal/modules/jsonapi/jsonapi.services.yml   | 122 +++
 drupal/modules/jsonapi/phpcs.xml              |  14 +
 drupal/modules/jsonapi/schema.json            | 375 ++++++++
 .../CustomQueryParameterNamesAccessCheck.php  |  59 ++
 .../jsonapi/src/Context/CurrentContext.php    | 150 +++
 .../jsonapi/src/Context/FieldResolver.php     | 188 ++++
 .../jsonapi/src/Controller/EntityResource.php | 808 ++++++++++++++++
 .../jsonapi/src/Controller/EntryPoint.php     |  92 ++
 .../jsonapi/src/Controller/RequestHandler.php | 218 +++++
 .../RemoveJsonapiFormatCompilerPass.php       |  51 +
 .../jsonapi/src/Encoder/JsonEncoder.php       |  43 +
 .../modules/jsonapi/src/EntityToJsonApi.php   | 107 +++
 .../DefaultExceptionSubscriber.php            |  80 ++
 .../ResourceResponseSubscriber.php            | 215 +++++
 .../EntityAccessDeniedHttpException.php       |  72 ++
 .../UnprocessableHttpEntityException.php      |  65 ++
 .../jsonapi/src/Field/FileDownloadUrl.php     |  90 ++
 drupal/modules/jsonapi/src/JsonApiSpec.php    | 117 +++
 .../jsonapi/src/JsonapiServiceProvider.php    |  41 +
 .../jsonapi/src/LinkManager/LinkManager.php   | 205 ++++
 .../src/Normalizer/ConfigEntityNormalizer.php |  62 ++
 .../Normalizer/ContentEntityNormalizer.php    |   8 +
 ...ityAccessDeniedHttpExceptionNormalizer.php |  51 +
 .../src/Normalizer/EntityNormalizer.php       | 260 ++++++
 .../EntityReferenceFieldNormalizer.php        | 222 +++++
 .../src/Normalizer/FieldItemNormalizer.php    |  53 ++
 .../src/Normalizer/FieldNormalizer.php        |  69 ++
 .../Normalizer/HttpExceptionNormalizer.php    | 153 +++
 .../JsonApiDocumentTopLevelNormalizer.php     | 263 ++++++
 .../jsonapi/src/Normalizer/NormalizerBase.php |  51 +
 .../jsonapi/src/Normalizer/Relationship.php   | 145 +++
 .../src/Normalizer/RelationshipItem.php       | 114 +++
 .../Normalizer/RelationshipItemNormalizer.php | 139 +++
 .../src/Normalizer/RelationshipNormalizer.php | 118 +++
 .../src/Normalizer/ScalarNormalizer.php       |  41 +
 ...ocessableHttpEntityExceptionNormalizer.php |  84 ++
 .../Value/EntityNormalizerValue.php           | 147 +++
 .../Value/FieldItemNormalizerValue.php        | 107 +++
 .../Normalizer/Value/FieldNormalizerValue.php | 128 +++
 .../Value/FieldNormalizerValueInterface.php   |  56 ++
 .../Value/HttpExceptionNormalizerValue.php    |   8 +
 ...JsonApiDocumentTopLevelNormalizerValue.php | 179 ++++
 .../Value/NullFieldNormalizerValue.php        |  70 ++
 .../Value/RelationshipItemNormalizerValue.php |  64 ++
 .../Value/RelationshipNormalizerValue.php     |  95 ++
 .../Value/ValueExtractorInterface.php         |  26 +
 .../ParamConverter/EntityUuidConverter.php    |  48 +
 .../jsonapi/src/Query/ConditionOption.php     | 101 ++
 .../modules/jsonapi/src/Query/GroupOption.php | 153 +++
 .../jsonapi/src/Query/OffsetPagerOption.php   |  57 ++
 .../jsonapi/src/Query/QueryBuilder.php        | 362 +++++++
 .../src/Query/QueryOptionInterface.php        |  29 +
 .../Query/QueryOptionTreeItemInterface.php    |  28 +
 .../modules/jsonapi/src/Query/SortOption.php  |  71 ++
 .../jsonapi/src/Resource/EntityCollection.php | 110 +++
 .../src/Resource/JsonApiDocumentTopLevel.php  |  45 +
 .../modules/jsonapi/src/ResourceResponse.php  |  57 ++
 .../jsonapi/src/ResourceType/ResourceType.php | 165 ++++
 .../ResourceType/ResourceTypeRepository.php   | 124 +++
 .../src/Routing/JsonApiParamEnhancer.php      |  66 ++
 .../jsonapi/src/Routing/Param/Filter.php      | 310 ++++++
 .../src/Routing/Param/JsonApiParamBase.php    |  69 ++
 .../Routing/Param/JsonApiParamInterface.php   |  44 +
 .../jsonapi/src/Routing/Param/OffsetPage.php  |  74 ++
 .../jsonapi/src/Routing/Param/Sort.php        | 131 +++
 .../jsonapi/src/Routing/RouteEnhancer.php     |  43 +
 drupal/modules/jsonapi/src/Routing/Routes.php | 187 ++++
 .../src/StackMiddleware/FormatSetter.php      |  64 ++
 drupal/modules/jsonapi/tests/phpunit.xml      |  18 +
 .../JsonApiFunctionalMultilingualTest.php     |  67 ++
 .../src/Functional/JsonApiFunctionalTest.php  | 595 ++++++++++++
 .../Functional/JsonApiFunctionalTestBase.php  | 267 ++++++
 .../src/Functional/RestJsonApiUnsupported.php | 108 +++
 .../src/Kernel/Context/FieldResolverTest.php  | 199 ++++
 .../Kernel/Controller/EntityResourceTest.php  | 880 ++++++++++++++++++
 .../src/Kernel/Controller/EntryPointTest.php  |  47 +
 .../tests/src/Kernel/EntityToJsonApiTest.php  | 200 ++++
 .../src/Kernel/Field/FileDownloadUrlTest.php  |  73 ++
 .../src/Kernel/JsonapiKernelTestBase.php      |  70 ++
 .../JsonApiDocumentTopLevelNormalizerTest.php | 703 ++++++++++++++
 .../ResourceTypeRepositoryTest.php            |  97 ++
 ...stomQueryParameterNamesAccessCheckTest.php |  50 +
 .../src/Unit/Context/CurrentContextTest.php   | 155 +++
 .../Unit/Controller/RequestHandlerTest.php    |  61 ++
 .../ResourceResponseSubscriberTest.php        | 107 +++
 .../tests/src/Unit/JsonApiSpecTest.php        | 114 +++
 .../src/Unit/LinkManager/LinkManagerTest.php  | 187 ++++
 .../Normalizer/ConfigEntityNormalizerTest.php |  89 ++
 .../EntityReferenceFieldNormalizerTest.php    | 159 ++++
 .../HttpExceptionNormalizerTest.php           |  45 +
 .../JsonApiDocumentTopLevelNormalizerTest.php | 177 ++++
 .../Value/EntityNormalizerValueTest.php       | 156 ++++
 .../Value/FieldItemNormalizerValueTest.php    |  46 +
 .../Value/FieldNormalizerValueTest.php        |  50 +
 ...ApiDocumentTopLevelNormalizerValueTest.php | 121 +++
 .../RelationshipItemNormalizerValueTest.php   |  37 +
 .../Value/RelationshipNormalizerValueTest.php |  94 ++
 .../Unit/Routing/JsonApiParamEnhancerTest.php |  96 ++
 .../src/Unit/Routing/Param/FilterTest.php     | 349 +++++++
 .../src/Unit/Routing/Param/OffsetPageTest.php |  45 +
 .../tests/src/Unit/Routing/Param/SortTest.php |  75 ++
 .../tests/src/Unit/Routing/RoutesTest.php     | 128 +++
 drupal/modules/jsonb/LICENSE.txt              | 339 +++++++
 .../jsonb/config/schema/jsonb.schema.yml      |   9 +
 drupal/modules/jsonb/jsonb.info.yml           |  11 +
 drupal/modules/jsonb/jsonb.install            |  13 +
 drupal/modules/jsonb/jsonb.module             |   2 +
 .../FieldFormatter/JsonbDefaultFormatter.php  |  44 +
 .../src/Plugin/Field/FieldType/JsonItem.php   |  60 ++
 .../src/Plugin/Field/FieldType/JsonbItem.php  |  51 +
 .../Plugin/Field/FieldWidget/JsonbWidget.php  |  55 ++
 drupal/modules/restui/.gitignore              |   1 +
 drupal/modules/restui/LICENSE.txt             | 339 +++++++
 drupal/modules/restui/README.txt              |  11 +
 drupal/modules/restui/restui.info.yml         |  15 +
 drupal/modules/restui/restui.links.menu.yml   |   5 +
 drupal/modules/restui/restui.module           |  34 +
 drupal/modules/restui/restui.post_update.php  |  20 +
 drupal/modules/restui/restui.routing.yml      |  21 +
 .../src/Controller/RestUIController.php       | 300 ++++++
 drupal/modules/restui/src/Form/RestUIForm.php | 483 ++++++++++
 .../templates/restui-resource-info.html.twig  |  27 +
 .../tests/src/Functional/RestUITest.php       |  82 ++
 261 files changed, 25979 insertions(+)
 create mode 100755 drupal/modules/README.txt
 create mode 160000 drupal/modules/contrib/consumers
 create mode 100644 drupal/modules/contrib/simple_oauth/.travis.yml
 create mode 100644 drupal/modules/contrib/simple_oauth/LICENSE.txt
 create mode 100644 drupal/modules/contrib/simple_oauth/README.md
 create mode 100644 drupal/modules/contrib/simple_oauth/composer.json
 create mode 100644 drupal/modules/contrib/simple_oauth/config/install/simple_oauth.oauth2_token.bundle.access_token.yml
 create mode 100644 drupal/modules/contrib/simple_oauth/config/install/simple_oauth.oauth2_token.bundle.auth_code.yml
 create mode 100644 drupal/modules/contrib/simple_oauth/config/install/simple_oauth.oauth2_token.bundle.refresh_token.yml
 create mode 100644 drupal/modules/contrib/simple_oauth/config/install/simple_oauth.settings.yml
 create mode 100644 drupal/modules/contrib/simple_oauth/config/schema/simple_oauth.schema.yml
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth.info.yml
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth.install
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth.links.action.yml
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth.links.menu.yml
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth.links.task.yml
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth.module
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth.permissions.yml
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth.routing.yml
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth.services.yml
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth_extras/config/install/simple_oauth_extras.settings.yml
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth_extras/config/schema/simple_oauth_extras.schema.yml
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth_extras/simple_oauth_extras.info.yml
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth_extras/simple_oauth_extras.module
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth_extras/simple_oauth_extras.permissions.yml
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth_extras/simple_oauth_extras.routing.yml
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth_extras/simple_oauth_extras.services.yml
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Controller/DebugController.php
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Controller/Oauth2AuthorizeForm.php
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Entities/AuthCodeEntity.php
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Plugin/Oauth2Grant/AuthorizationCode.php
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Plugin/Oauth2Grant/ClientCredentials.php
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Plugin/Oauth2Grant/Code.php
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Plugin/Oauth2Grant/Implicit.php
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Plugin/Oauth2Grant/RefreshToken.php
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Repositories/AuthCodeRepository.php
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth_extras/tests/simple_oauth_extras_test/simple_oauth_extras_test.info.yml
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth_extras/tests/simple_oauth_extras_test/simple_oauth_extras_test.routing.yml
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth_extras/tests/simple_oauth_extras_test/src/Controller/RedirectDebug.php
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth_extras/tests/src/Functional/AuthCodeFunctionalTest.php
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth_extras/tests/src/Functional/ClientCredentialsFunctionalTest.php
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth_extras/tests/src/Functional/ImplicitFunctionalTest.php
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth_extras/tests/src/Functional/RefreshFunctionalTest.php
 create mode 100644 drupal/modules/contrib/simple_oauth/simple_oauth_extras/tests/src/Functional/RolesNegotiationFunctionalTest.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/AccessTokenAccessControlHandler.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Annotation/Oauth2Grant.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Authentication/Provider/SimpleOauthAuthenticationProvider.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Authentication/Provider/SimpleOauthAuthenticationProviderInterface.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Authentication/TokenAuthUser.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Authentication/TokenAuthUserInterface.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Controller/Oauth2Token.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Entities/AccessTokenEntity.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Entities/ClientEntity.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Entities/ClientEntityInterface.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Entities/RefreshTokenEntity.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Entities/ScopeEntity.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Entities/UserEntity.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Entity/ConfigEntityLockableInterface.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Entity/ConfigEntityLockableTrait.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Entity/Form/Oauth2TokenDeleteForm.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Entity/Form/Oauth2TokenSettingsForm.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Entity/Oauth2Token.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Entity/Oauth2TokenInterface.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Entity/Oauth2TokenType.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Entity/Oauth2TokenTypeInterface.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/ExpiredCollector.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/HttpMiddleware/BasicAuthSwap.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/LockableConfigEntityAccessControlHandler.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Normalizer/RefreshTokenEntityNormalizer.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Normalizer/TokenEntityNormalizer.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Normalizer/TokenEntityNormalizerInterface.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Oauth2TokenListBuilder.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/PageCache/DisallowSimpleOauthRequests.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Plugin/Oauth2Grant/Password.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Plugin/Oauth2GrantBase.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Plugin/Oauth2GrantInterface.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Plugin/Oauth2GrantManager.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Plugin/Oauth2GrantManagerInterface.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Repositories/AccessTokenRepository.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Repositories/ClientRepository.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Repositories/RefreshTokenRepository.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Repositories/RevocableTokenRepositoryTrait.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Repositories/ScopeRepository.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Repositories/UserRepository.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Server/ResourceServer.php
 create mode 100644 drupal/modules/contrib/simple_oauth/src/Server/ResourceServerInterface.php
 create mode 100644 drupal/modules/contrib/simple_oauth/tests/certificates/private.key
 create mode 100644 drupal/modules/contrib/simple_oauth/tests/certificates/public.key
 create mode 100644 drupal/modules/contrib/simple_oauth/tests/src/Functional/PasswordFunctionalTest.php
 create mode 100644 drupal/modules/contrib/simple_oauth/tests/src/Functional/RequestHelperTrait.php
 create mode 100644 drupal/modules/contrib/simple_oauth/tests/src/Functional/TokenBearerFunctionalTestBase.php
 create mode 100644 drupal/modules/contrib/simple_oauth/tests/src/Unit/Authentication/Provider/SimpleOauthAuthenticationTest.php
 create mode 100644 drupal/modules/contrib/simple_oauth/tests/src/Unit/EntityCollectorTest.php
 create mode 100755 drupal/modules/custom/datacenter/admin_area_entity.page.inc
 create mode 100755 drupal/modules/custom/datacenter/composer.json
 create mode 100755 drupal/modules/custom/datacenter/config/install/views.view.frontistiria.yml
 create mode 100755 drupal/modules/custom/datacenter/config/install/views.view.get_areas.yml
 create mode 100755 drupal/modules/custom/datacenter/datacenter.info.yml
 create mode 100755 drupal/modules/custom/datacenter/datacenter.links.action.yml
 create mode 100755 drupal/modules/custom/datacenter/datacenter.links.menu.yml
 create mode 100755 drupal/modules/custom/datacenter/datacenter.links.task.yml
 create mode 100755 drupal/modules/custom/datacenter/datacenter.module
 create mode 100755 drupal/modules/custom/datacenter/datacenter.permissions.yml
 create mode 100644 drupal/modules/custom/datacenter/licenses_entity.page.inc
 create mode 100755 drupal/modules/custom/datacenter/src/AdminAreaEntityAccessControlHandler.php
 create mode 100755 drupal/modules/custom/datacenter/src/AdminAreaEntityHtmlRouteProvider.php
 create mode 100755 drupal/modules/custom/datacenter/src/AdminAreaEntityListBuilder.php
 create mode 100755 drupal/modules/custom/datacenter/src/Entity/AdminAreaEntity.php
 create mode 100755 drupal/modules/custom/datacenter/src/Entity/AdminAreaEntityInterface.php
 create mode 100755 drupal/modules/custom/datacenter/src/Entity/AdminAreaEntityViewsData.php
 create mode 100644 drupal/modules/custom/datacenter/src/Entity/LicensesEntity.php
 create mode 100644 drupal/modules/custom/datacenter/src/Entity/LicensesEntityInterface.php
 create mode 100644 drupal/modules/custom/datacenter/src/Entity/LicensesEntityViewsData.php
 create mode 100755 drupal/modules/custom/datacenter/src/Form/AdminAreaEntityDeleteForm.php
 create mode 100755 drupal/modules/custom/datacenter/src/Form/AdminAreaEntityForm.php
 create mode 100755 drupal/modules/custom/datacenter/src/Form/AdminAreaEntitySettingsForm.php
 create mode 100644 drupal/modules/custom/datacenter/src/Form/LicensesEntityDeleteForm.php
 create mode 100644 drupal/modules/custom/datacenter/src/Form/LicensesEntityForm.php
 create mode 100644 drupal/modules/custom/datacenter/src/Form/LicensesEntitySettingsForm.php
 create mode 100644 drupal/modules/custom/datacenter/src/LicensesEntityAccessControlHandler.php
 create mode 100644 drupal/modules/custom/datacenter/src/LicensesEntityHtmlRouteProvider.php
 create mode 100644 drupal/modules/custom/datacenter/src/LicensesEntityListBuilder.php
 create mode 100755 drupal/modules/custom/datacenter/templates/admin_area_entity.html.twig
 create mode 100755 drupal/modules/custom/datacenter/templates/datacenter.html.twig
 create mode 100644 drupal/modules/custom/datacenter/templates/licenses_entity.html.twig
 create mode 100755 drupal/modules/custom/datachanger/composer.json
 create mode 100755 drupal/modules/custom/datachanger/datachanger.info.yml
 create mode 100755 drupal/modules/custom/datachanger/datachanger.module
 create mode 100755 drupal/modules/custom/datachanger/datachanger.routing.yml
 create mode 100755 drupal/modules/custom/datachanger/src/Controller/DataController.php
 create mode 100755 drupal/modules/custom/datachanger/templates/datachanger.html.twig
 create mode 100644 drupal/modules/jsonapi/LICENSE.txt
 create mode 100644 drupal/modules/jsonapi/README.md
 create mode 100644 drupal/modules/jsonapi/composer.json
 create mode 100644 drupal/modules/jsonapi/jsonapi.info.yml
 create mode 100644 drupal/modules/jsonapi/jsonapi.module
 create mode 100644 drupal/modules/jsonapi/jsonapi.permissions.yml
 create mode 100644 drupal/modules/jsonapi/jsonapi.routing.yml
 create mode 100644 drupal/modules/jsonapi/jsonapi.services.yml
 create mode 100644 drupal/modules/jsonapi/phpcs.xml
 create mode 100644 drupal/modules/jsonapi/schema.json
 create mode 100644 drupal/modules/jsonapi/src/Access/CustomQueryParameterNamesAccessCheck.php
 create mode 100644 drupal/modules/jsonapi/src/Context/CurrentContext.php
 create mode 100644 drupal/modules/jsonapi/src/Context/FieldResolver.php
 create mode 100644 drupal/modules/jsonapi/src/Controller/EntityResource.php
 create mode 100644 drupal/modules/jsonapi/src/Controller/EntryPoint.php
 create mode 100644 drupal/modules/jsonapi/src/Controller/RequestHandler.php
 create mode 100644 drupal/modules/jsonapi/src/DependencyInjection/Compiler/RemoveJsonapiFormatCompilerPass.php
 create mode 100644 drupal/modules/jsonapi/src/Encoder/JsonEncoder.php
 create mode 100644 drupal/modules/jsonapi/src/EntityToJsonApi.php
 create mode 100644 drupal/modules/jsonapi/src/EventSubscriber/DefaultExceptionSubscriber.php
 create mode 100644 drupal/modules/jsonapi/src/EventSubscriber/ResourceResponseSubscriber.php
 create mode 100644 drupal/modules/jsonapi/src/Exception/EntityAccessDeniedHttpException.php
 create mode 100644 drupal/modules/jsonapi/src/Exception/UnprocessableHttpEntityException.php
 create mode 100644 drupal/modules/jsonapi/src/Field/FileDownloadUrl.php
 create mode 100644 drupal/modules/jsonapi/src/JsonApiSpec.php
 create mode 100644 drupal/modules/jsonapi/src/JsonapiServiceProvider.php
 create mode 100644 drupal/modules/jsonapi/src/LinkManager/LinkManager.php
 create mode 100644 drupal/modules/jsonapi/src/Normalizer/ConfigEntityNormalizer.php
 create mode 100644 drupal/modules/jsonapi/src/Normalizer/ContentEntityNormalizer.php
 create mode 100644 drupal/modules/jsonapi/src/Normalizer/EntityAccessDeniedHttpExceptionNormalizer.php
 create mode 100644 drupal/modules/jsonapi/src/Normalizer/EntityNormalizer.php
 create mode 100644 drupal/modules/jsonapi/src/Normalizer/EntityReferenceFieldNormalizer.php
 create mode 100644 drupal/modules/jsonapi/src/Normalizer/FieldItemNormalizer.php
 create mode 100644 drupal/modules/jsonapi/src/Normalizer/FieldNormalizer.php
 create mode 100644 drupal/modules/jsonapi/src/Normalizer/HttpExceptionNormalizer.php
 create mode 100644 drupal/modules/jsonapi/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php
 create mode 100644 drupal/modules/jsonapi/src/Normalizer/NormalizerBase.php
 create mode 100644 drupal/modules/jsonapi/src/Normalizer/Relationship.php
 create mode 100644 drupal/modules/jsonapi/src/Normalizer/RelationshipItem.php
 create mode 100644 drupal/modules/jsonapi/src/Normalizer/RelationshipItemNormalizer.php
 create mode 100644 drupal/modules/jsonapi/src/Normalizer/RelationshipNormalizer.php
 create mode 100644 drupal/modules/jsonapi/src/Normalizer/ScalarNormalizer.php
 create mode 100644 drupal/modules/jsonapi/src/Normalizer/UnprocessableHttpEntityExceptionNormalizer.php
 create mode 100644 drupal/modules/jsonapi/src/Normalizer/Value/EntityNormalizerValue.php
 create mode 100644 drupal/modules/jsonapi/src/Normalizer/Value/FieldItemNormalizerValue.php
 create mode 100644 drupal/modules/jsonapi/src/Normalizer/Value/FieldNormalizerValue.php
 create mode 100644 drupal/modules/jsonapi/src/Normalizer/Value/FieldNormalizerValueInterface.php
 create mode 100644 drupal/modules/jsonapi/src/Normalizer/Value/HttpExceptionNormalizerValue.php
 create mode 100644 drupal/modules/jsonapi/src/Normalizer/Value/JsonApiDocumentTopLevelNormalizerValue.php
 create mode 100644 drupal/modules/jsonapi/src/Normalizer/Value/NullFieldNormalizerValue.php
 create mode 100644 drupal/modules/jsonapi/src/Normalizer/Value/RelationshipItemNormalizerValue.php
 create mode 100644 drupal/modules/jsonapi/src/Normalizer/Value/RelationshipNormalizerValue.php
 create mode 100644 drupal/modules/jsonapi/src/Normalizer/Value/ValueExtractorInterface.php
 create mode 100644 drupal/modules/jsonapi/src/ParamConverter/EntityUuidConverter.php
 create mode 100644 drupal/modules/jsonapi/src/Query/ConditionOption.php
 create mode 100644 drupal/modules/jsonapi/src/Query/GroupOption.php
 create mode 100644 drupal/modules/jsonapi/src/Query/OffsetPagerOption.php
 create mode 100644 drupal/modules/jsonapi/src/Query/QueryBuilder.php
 create mode 100644 drupal/modules/jsonapi/src/Query/QueryOptionInterface.php
 create mode 100644 drupal/modules/jsonapi/src/Query/QueryOptionTreeItemInterface.php
 create mode 100644 drupal/modules/jsonapi/src/Query/SortOption.php
 create mode 100644 drupal/modules/jsonapi/src/Resource/EntityCollection.php
 create mode 100644 drupal/modules/jsonapi/src/Resource/JsonApiDocumentTopLevel.php
 create mode 100644 drupal/modules/jsonapi/src/ResourceResponse.php
 create mode 100644 drupal/modules/jsonapi/src/ResourceType/ResourceType.php
 create mode 100644 drupal/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php
 create mode 100644 drupal/modules/jsonapi/src/Routing/JsonApiParamEnhancer.php
 create mode 100644 drupal/modules/jsonapi/src/Routing/Param/Filter.php
 create mode 100644 drupal/modules/jsonapi/src/Routing/Param/JsonApiParamBase.php
 create mode 100644 drupal/modules/jsonapi/src/Routing/Param/JsonApiParamInterface.php
 create mode 100644 drupal/modules/jsonapi/src/Routing/Param/OffsetPage.php
 create mode 100644 drupal/modules/jsonapi/src/Routing/Param/Sort.php
 create mode 100644 drupal/modules/jsonapi/src/Routing/RouteEnhancer.php
 create mode 100644 drupal/modules/jsonapi/src/Routing/Routes.php
 create mode 100644 drupal/modules/jsonapi/src/StackMiddleware/FormatSetter.php
 create mode 100644 drupal/modules/jsonapi/tests/phpunit.xml
 create mode 100644 drupal/modules/jsonapi/tests/src/Functional/JsonApiFunctionalMultilingualTest.php
 create mode 100644 drupal/modules/jsonapi/tests/src/Functional/JsonApiFunctionalTest.php
 create mode 100644 drupal/modules/jsonapi/tests/src/Functional/JsonApiFunctionalTestBase.php
 create mode 100644 drupal/modules/jsonapi/tests/src/Functional/RestJsonApiUnsupported.php
 create mode 100644 drupal/modules/jsonapi/tests/src/Kernel/Context/FieldResolverTest.php
 create mode 100644 drupal/modules/jsonapi/tests/src/Kernel/Controller/EntityResourceTest.php
 create mode 100644 drupal/modules/jsonapi/tests/src/Kernel/Controller/EntryPointTest.php
 create mode 100644 drupal/modules/jsonapi/tests/src/Kernel/EntityToJsonApiTest.php
 create mode 100644 drupal/modules/jsonapi/tests/src/Kernel/Field/FileDownloadUrlTest.php
 create mode 100644 drupal/modules/jsonapi/tests/src/Kernel/JsonapiKernelTestBase.php
 create mode 100644 drupal/modules/jsonapi/tests/src/Kernel/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php
 create mode 100644 drupal/modules/jsonapi/tests/src/Kernel/ResourceType/ResourceTypeRepositoryTest.php
 create mode 100644 drupal/modules/jsonapi/tests/src/Unit/Access/CustomQueryParameterNamesAccessCheckTest.php
 create mode 100644 drupal/modules/jsonapi/tests/src/Unit/Context/CurrentContextTest.php
 create mode 100644 drupal/modules/jsonapi/tests/src/Unit/Controller/RequestHandlerTest.php
 create mode 100644 drupal/modules/jsonapi/tests/src/Unit/EventSubscriber/ResourceResponseSubscriberTest.php
 create mode 100644 drupal/modules/jsonapi/tests/src/Unit/JsonApiSpecTest.php
 create mode 100644 drupal/modules/jsonapi/tests/src/Unit/LinkManager/LinkManagerTest.php
 create mode 100644 drupal/modules/jsonapi/tests/src/Unit/Normalizer/ConfigEntityNormalizerTest.php
 create mode 100644 drupal/modules/jsonapi/tests/src/Unit/Normalizer/EntityReferenceFieldNormalizerTest.php
 create mode 100644 drupal/modules/jsonapi/tests/src/Unit/Normalizer/HttpExceptionNormalizerTest.php
 create mode 100644 drupal/modules/jsonapi/tests/src/Unit/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php
 create mode 100644 drupal/modules/jsonapi/tests/src/Unit/Normalizer/Value/EntityNormalizerValueTest.php
 create mode 100644 drupal/modules/jsonapi/tests/src/Unit/Normalizer/Value/FieldItemNormalizerValueTest.php
 create mode 100644 drupal/modules/jsonapi/tests/src/Unit/Normalizer/Value/FieldNormalizerValueTest.php
 create mode 100644 drupal/modules/jsonapi/tests/src/Unit/Normalizer/Value/JsonApiDocumentTopLevelNormalizerValueTest.php
 create mode 100644 drupal/modules/jsonapi/tests/src/Unit/Normalizer/Value/RelationshipItemNormalizerValueTest.php
 create mode 100644 drupal/modules/jsonapi/tests/src/Unit/Normalizer/Value/RelationshipNormalizerValueTest.php
 create mode 100644 drupal/modules/jsonapi/tests/src/Unit/Routing/JsonApiParamEnhancerTest.php
 create mode 100644 drupal/modules/jsonapi/tests/src/Unit/Routing/Param/FilterTest.php
 create mode 100644 drupal/modules/jsonapi/tests/src/Unit/Routing/Param/OffsetPageTest.php
 create mode 100644 drupal/modules/jsonapi/tests/src/Unit/Routing/Param/SortTest.php
 create mode 100644 drupal/modules/jsonapi/tests/src/Unit/Routing/RoutesTest.php
 create mode 100755 drupal/modules/jsonb/LICENSE.txt
 create mode 100755 drupal/modules/jsonb/config/schema/jsonb.schema.yml
 create mode 100755 drupal/modules/jsonb/jsonb.info.yml
 create mode 100755 drupal/modules/jsonb/jsonb.install
 create mode 100755 drupal/modules/jsonb/jsonb.module
 create mode 100755 drupal/modules/jsonb/src/Plugin/Field/FieldFormatter/JsonbDefaultFormatter.php
 create mode 100755 drupal/modules/jsonb/src/Plugin/Field/FieldType/JsonItem.php
 create mode 100755 drupal/modules/jsonb/src/Plugin/Field/FieldType/JsonbItem.php
 create mode 100755 drupal/modules/jsonb/src/Plugin/Field/FieldWidget/JsonbWidget.php
 create mode 100755 drupal/modules/restui/.gitignore
 create mode 100755 drupal/modules/restui/LICENSE.txt
 create mode 100755 drupal/modules/restui/README.txt
 create mode 100755 drupal/modules/restui/restui.info.yml
 create mode 100755 drupal/modules/restui/restui.links.menu.yml
 create mode 100755 drupal/modules/restui/restui.module
 create mode 100755 drupal/modules/restui/restui.post_update.php
 create mode 100755 drupal/modules/restui/restui.routing.yml
 create mode 100755 drupal/modules/restui/src/Controller/RestUIController.php
 create mode 100755 drupal/modules/restui/src/Form/RestUIForm.php
 create mode 100755 drupal/modules/restui/templates/restui-resource-info.html.twig
 create mode 100755 drupal/modules/restui/tests/src/Functional/RestUITest.php

diff --git a/drupal/modules/README.txt b/drupal/modules/README.txt
new file mode 100755
index 0000000..529c31b
--- /dev/null
+++ b/drupal/modules/README.txt
@@ -0,0 +1,42 @@
+Modules extend your site functionality beyond Drupal core.
+
+WHAT TO PLACE IN THIS DIRECTORY?
+--------------------------------
+
+Placing downloaded and custom modules in this directory separates downloaded and
+custom modules from Drupal core's modules. This allows Drupal core to be updated
+without overwriting these files.
+
+DOWNLOAD ADDITIONAL MODULES
+---------------------------
+
+Contributed modules from the Drupal community may be downloaded at
+https://www.drupal.org/project/project_module.
+
+ORGANIZING MODULES IN THIS DIRECTORY
+------------------------------------
+
+You may create subdirectories in this directory, to organize your added modules,
+without breaking the site. Some common subdirectories include "contrib" for
+contributed modules, and "custom" for custom modules. Note that if you move a
+module to a subdirectory after it has been enabled, you may need to clear the
+Drupal cache so it can be found.
+
+There are number of directories that are ignored when looking for modules. These
+are 'src', 'lib', 'vendor', 'assets', 'css', 'files', 'images', 'js', 'misc',
+'templates', 'includes', 'fixtures' and 'Drupal'.
+
+MULTISITE CONFIGURATION
+-----------------------
+
+In multisite configurations, modules found in this directory are available to
+all sites. You may also put modules in the sites/all/modules directory, and the
+versions in sites/all/modules will take precedence over versions of the same
+module that are here. Alternatively, the sites/your_site_name/modules directory
+pattern may be used to restrict modules to a specific site instance.
+
+MORE INFORMATION
+----------------
+
+Refer to the β€œDeveloping for Drupal” section of the README.txt in the Drupal
+root directory for further information on extending Drupal with custom modules.
diff --git a/drupal/modules/contrib/consumers b/drupal/modules/contrib/consumers
new file mode 160000
index 0000000..ef5644c
--- /dev/null
+++ b/drupal/modules/contrib/consumers
@@ -0,0 +1 @@
+Subproject commit ef5644c2a46ccaf527f924b6360f1c366ba876d5
diff --git a/drupal/modules/contrib/simple_oauth/.travis.yml b/drupal/modules/contrib/simple_oauth/.travis.yml
new file mode 100644
index 0000000..a4c3f7b
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/.travis.yml
@@ -0,0 +1,67 @@
+language: php
+sudo: false
+
+php:
+  - 5.6
+  - 7
+
+env:
+  - DRUPAL_CORE=8.2.x
+  - DRUPAL_CORE=8.3.x
+
+matrix:
+  fast_finish: true
+
+mysql:
+  database: drupal
+  username: root
+  encoding: utf8
+
+# Cache composer downloads because cloning Coder form drupal.org is very slow :-(
+cache:
+  directories:
+    - $HOME/.composer
+
+before_script:
+  # Remove Xdebug as we don't need it and it causes
+  # PHP Fatal error:  Maximum function nesting level of '256' reached.
+  # We also don't care if that file exists or not on PHP 7.
+  - phpenv config-rm xdebug.ini || true
+
+  # Remember the current simple_oauth test directory for later use in the Drupal
+  # installation.
+  - TESTDIR=$(pwd)
+  # Navigate out of module directory to prevent blown stack by recursive module
+  # lookup.
+  - cd ..
+
+  # Create database.
+  - mysql -e 'create database drupal'
+  # Export database variable for kernel tests.
+  - export SIMPLETEST_DB=mysql://root:@127.0.0.1/drupal
+  - travis_retry git clone --branch $DRUPAL_CORE --depth 1 http://git.drupal.org/project/drupal.git
+  - cd drupal
+
+  # Reference simple_oauth in build site.
+  - ln -s $TESTDIR modules/simple_oauth
+
+  - travis_retry composer self-update
+  - travis_retry composer install
+  # Add dependency manually since installing via git pull is not bringing that.
+  - travis_retry composer require league/oauth2-server:^5.1
+
+  # Start a web server on port 8888, run in the background.
+  - php -S localhost:8888 &
+
+  # Export web server URL for browser tests.
+  - export SIMPLETEST_BASE_URL=http://localhost:8888
+
+  # Install PHPCS to check for Drupal coding standards.
+  - travis_retry composer global require drupal/coder
+  - ~/.composer/vendor/bin/phpcs --config-set installed_paths ~/.composer/vendor/drupal/coder/coder_sniffer
+
+script:
+  # Run the PHPUnit tests which also include the kernel tests.
+  - ./vendor/bin/phpunit --verbose --color -c ./core/phpunit.xml.dist ./modules/simple_oauth
+  # Check for coding standards violations
+  - cd modules/simple_oauth && ~/.composer/vendor/bin/phpcs --standard=DrupalPractice .
diff --git a/drupal/modules/contrib/simple_oauth/LICENSE.txt b/drupal/modules/contrib/simple_oauth/LICENSE.txt
new file mode 100644
index 0000000..d159169
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/LICENSE.txt
@@ -0,0 +1,339 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/drupal/modules/contrib/simple_oauth/README.md b/drupal/modules/contrib/simple_oauth/README.md
new file mode 100644
index 0000000..68dba5d
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/README.md
@@ -0,0 +1,59 @@
+[![Build Status](https://travis-ci.org/e0ipso/simple_oauth.svg?branch=8.x-2.x)](https://travis-ci.org/e0ipso/simple_oauth)
+
+Simple OAuth is an implementation of the [OAuth 2.0 Authorization Framework RFC](https://tools.ietf.org/html/rfc6749). Using OAuth 2.0 Bearer Token is very easy. See how you can get the basics working in **less than 5 minutes**! This project is focused in simplicity of use and flexibility. When deciding which project to use, also consider other projects like [OAuth](https://www.drupal.org/project/oauth), an OAuth 1 implementation that doesn't rely on you having https in your production server.
+
+### Based on League\OAuth2
+This module uses the fantastic PHP library [OAuth 2.0 Server](http://oauth2.thephpleague.com) from [The League of Extraordinary Packages](http://thephpleague.com). This library has become the de-facto standard for modern PHP applications and is thoroughly tested.
+
+[![Latest Version](http://img.shields.io/packagist/v/league/oauth2-server.svg?style=flat-square)](https://github.com/thephpleague/oauth2-server/releases)
+[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md)
+[![Build Status](https://img.shields.io/travis/thephpleague/oauth2-server/master.svg?style=flat-square)](https://travis-ci.org/thephpleague/oauth2-server)
+[![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/thephpleague/oauth2-server.svg?style=flat-square)](https://scrutinizer-ci.com/g/thephpleague/oauth2-server/code-structure)
+[![Quality Score](https://img.shields.io/scrutinizer/g/thephpleague/oauth2-server.svg?style=flat-square)](https://scrutinizer-ci.com/g/thephpleague/oauth2-server)
+[![Total Downloads](https://img.shields.io/packagist/dt/league/oauth2-server.svg?style=flat-square)](https://packagist.org/packages/league/oauth2-server)
+
+### Quick demo (Password Grant)
+
+1. Install the module using Composer: `composer require drupal/simple_oauth:8.x-2.x`. You can use any other installation method, as long as you install the [OAuth2 Server](https://github.com/thephpleague/oauth2-server) composer package.
+2. Generate a pair of keys to encrypt the tokens. And store them outside of your document root for security reasons.
+```
+openssl genrsa -out private.key 2048
+openssl rsa -in private.key -pubout > public.key
+```
+3. Save the path to your keys in: `/admin/config/people/simple_oauth`.
+3. Go to [REST UI](https://drupal.org/project/restui) and enable the _oauth2_ authentication in your resource.
+4. Create a Client Application by going to: `/admin/config/services/consumer/add`.
+5. Create a token with your credentials by making a `POST` request to `/oauth/token`. See [the documentation](http://oauth2.thephpleague.com/authorization-server/resource-owner-password-credentials-grant/) about what fields your request should contain.
+6.  (Not shown) Permissions are set to only allow to view nodes via REST with the authenticated user.
+7.  Request a node via REST without authentication and watch it fail.
+8.  Request a node via REST with the header `Authorization: Bearer {YOUR_TOKEN}` and watch it succeed.
+
+![Simple OAuth animation](https://www.drupal.org/files/project-images/simple_oauth_2.gif)
+
+### Video tutorials
+
+[![](https://www.drupal.org/files/2015-12-10%2009-04-11.png)](https://youtu.be/kohs5MXESXc) Watch a detailed explanation on how to use this module in the video tutorials:
+
+1.  [Basic configuration.](https://youtu.be/kohs5MXESXc)
+2.  [Refresh your tokens.](https://youtu.be/E-wUKkQa1OM)
+3.  [Add extra security with resources.](https://youtu.be/PR0oBCCSxgE)
+
+### My token has expired!
+
+First, that is a good thing. Tokens are like cash, if you have it you can use it. You don't need to prove that token belongs to you, so don't let anyone steal your token. In order to lower the risk tokens should expire fairly quickly. If your token expires in 120s then it will be only usable during that window.
+
+#### What do I do if my token was expired?
+
+Along with your access token, an authentication token is created. It's called the _refresh token_ . It's a longer lived token, that it's associated to an access token and can be used to create a replica of your expired access token. You can then use that new access token normally. To use your refresh token you will need to make use of the [_Refresh Token Grant_](http://oauth2.thephpleague.com/authorization-server/refresh-token-grant/). That will return a JSON document with the new token and a new refresh token. That URL can only be accessed with your refresh token, even if your access token is still valid.
+
+#### What do I do if my refresh token was also expired, or I don't have a refresh token?
+
+Then you will need to generate a new token from scratch. You can avoid this by refreshing your access token before your refresh token expires. This way you avoid the need to require the user to prove their identity to Drupal to create a new token. Another way to mitigate this is to use longer expiration times in your tokens. This will work, but the the recommendation is to refresh your token in time.
+
+### Recommendation
+
+Check the official documentation on the [Bearer Token Usage](http://tools.ietf.org/html/rfc6750). And **turn on SSL!**.
+
+### Issues and contributions
+
+Issues and development happens in the [Drupal.org issue queue](https://www.drupal.org/project/issues/simple_oauth).
diff --git a/drupal/modules/contrib/simple_oauth/composer.json b/drupal/modules/contrib/simple_oauth/composer.json
new file mode 100644
index 0000000..130fdfc
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/composer.json
@@ -0,0 +1,15 @@
+{
+    "name": "drupal/simple_oauth",
+    "description": "The Simple OAuth module for Drupal",
+    "type": "drupal-module",
+    "require": {
+        "league/oauth2-server": "~6.0"
+    },
+    "license": "GPL-2.0+",
+    "authors": [
+        {
+            "name": "Mateu AguilΓ³ Bosch",
+            "email": "mateu.aguilo.bosch@gmail.com"
+        }
+    ]
+}
diff --git a/drupal/modules/contrib/simple_oauth/config/install/simple_oauth.oauth2_token.bundle.access_token.yml b/drupal/modules/contrib/simple_oauth/config/install/simple_oauth.oauth2_token.bundle.access_token.yml
new file mode 100644
index 0000000..afa1f11
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/config/install/simple_oauth.oauth2_token.bundle.access_token.yml
@@ -0,0 +1,6 @@
+langcode: en
+status: true
+id: access_token
+label: 'Access Token'
+description: 'The access token type.'
+locked: true
diff --git a/drupal/modules/contrib/simple_oauth/config/install/simple_oauth.oauth2_token.bundle.auth_code.yml b/drupal/modules/contrib/simple_oauth/config/install/simple_oauth.oauth2_token.bundle.auth_code.yml
new file mode 100644
index 0000000..5eace27
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/config/install/simple_oauth.oauth2_token.bundle.auth_code.yml
@@ -0,0 +1,6 @@
+langcode: en
+status: true
+id: auth_code
+label: 'Auth code'
+description: 'The auth code type.'
+locked: true
diff --git a/drupal/modules/contrib/simple_oauth/config/install/simple_oauth.oauth2_token.bundle.refresh_token.yml b/drupal/modules/contrib/simple_oauth/config/install/simple_oauth.oauth2_token.bundle.refresh_token.yml
new file mode 100644
index 0000000..436965b
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/config/install/simple_oauth.oauth2_token.bundle.refresh_token.yml
@@ -0,0 +1,6 @@
+langcode: en
+status: true
+id: refresh_token
+label: 'Refresh token'
+description: 'The refresh token type.'
+locked: true
diff --git a/drupal/modules/contrib/simple_oauth/config/install/simple_oauth.settings.yml b/drupal/modules/contrib/simple_oauth/config/install/simple_oauth.settings.yml
new file mode 100644
index 0000000..cb88901
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/config/install/simple_oauth.settings.yml
@@ -0,0 +1,2 @@
+access_token_expiration: 300
+refresh_token_expiration: 1209600
diff --git a/drupal/modules/contrib/simple_oauth/config/schema/simple_oauth.schema.yml b/drupal/modules/contrib/simple_oauth/config/schema/simple_oauth.schema.yml
new file mode 100644
index 0000000..b7fe8c7
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/config/schema/simple_oauth.schema.yml
@@ -0,0 +1,37 @@
+simple_oauth.oauth2_token.bundle.*:
+  type: config_entity
+  label: 'OAuth2 token type'
+  mapping:
+    id:
+      type: string
+      label: 'ID'
+    label:
+      type: label
+      label: 'Label'
+    description:
+      type: text
+      label: 'Description'
+    locked:
+      type: boolean
+      label: 'Locked'
+
+simple_oauth.settings:
+  type: config_object
+  label: 'Simple OAuth Settings'
+  mapping:
+    access_token_expiration:
+      type: integer
+      label: 'Access Token Expiration Time'
+      description: 'The default period in seconds while a access token is valid'
+    refresh_token_expiration:
+      type: integer
+      label: 'Refresh Token Expiration Time'
+      description: 'The default period in seconds while a refresh token is valid'
+    public_key:
+      type: path
+      label: 'Public Key'
+      description: 'The path to the public file.'
+    private_key:
+      type: path
+      label: 'Private Key'
+      description: 'The path to the private file.'
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth.info.yml b/drupal/modules/contrib/simple_oauth/simple_oauth.info.yml
new file mode 100644
index 0000000..eedc687
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth.info.yml
@@ -0,0 +1,16 @@
+name: Simple OAuth
+type: module
+description: 'The OAuth 2.0 Authorization Framework: Bearer Token Usage'
+# core: 8.x
+package: Authentication
+configure: oauth2_token.settings
+dependencies:
+  - drupal:system (>=8.3)
+  - serialization
+  - consumers
+
+# Information added by Drupal.org packaging script on 2017-09-10
+version: '8.x-3.0-beta1'
+core: '8.x'
+project: 'simple_oauth'
+datestamp: 1505028252
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth.install b/drupal/modules/contrib/simple_oauth/simple_oauth.install
new file mode 100644
index 0000000..598ef0f
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth.install
@@ -0,0 +1,13 @@
+<?php
+
+/**
+ * @file
+ * Module installation file.
+ */
+
+/**
+ * Enable the Simple OAuth Consumers module.
+ */
+function simple_oauth_update_8200() {
+  \Drupal::service('module_installer')->install(['consumers']);
+}
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth.links.action.yml b/drupal/modules/contrib/simple_oauth/simple_oauth.links.action.yml
new file mode 100644
index 0000000..b214c5a
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth.links.action.yml
@@ -0,0 +1,5 @@
+entity.simple_oauth.consumer.add_form:
+  route_name: 'entity.consumer.add_form'
+  title: 'Add Client'
+  appears_on:
+    - oauth2_token.settings
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth.links.menu.yml b/drupal/modules/contrib/simple_oauth/simple_oauth.links.menu.yml
new file mode 100644
index 0000000..d40300d
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth.links.menu.yml
@@ -0,0 +1,5 @@
+oauth2_token.admin.config.people.settings:
+  title: Simple OAuth
+  description: 'Configure the Simple OAuth settings and manage entities.'
+  route_name: oauth2_token.settings
+  parent: user.admin_index
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth.links.task.yml b/drupal/modules/contrib/simple_oauth/simple_oauth.links.task.yml
new file mode 100644
index 0000000..5af576e
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth.links.task.yml
@@ -0,0 +1,27 @@
+entity.consumer.collection:
+  title: 'Clients'
+  route_name: entity.consumer.collection
+  description: 'List Clients'
+  base_route: oauth2_token.settings
+
+# Access Token routing definition
+oauth2_token.settings_tab:
+  route_name: oauth2_token.settings
+  title: 'Settings'
+  base_route: oauth2_token.settings
+
+entity.oauth2_token.collection:
+  route_name: entity.oauth2_token.collection
+  base_route: oauth2_token.settings
+  title: 'Tokens'
+
+entity.oauth2_token.canonical:
+  route_name: entity.oauth2_token.canonical
+  base_route: entity.oauth2_token.canonical
+  title: 'View'
+
+entity.oauth2_token.delete_form:
+  route_name:  entity.oauth2_token.delete_form
+  base_route:  entity.oauth2_token.canonical
+  title: Delete
+  weight: 10
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth.module b/drupal/modules/contrib/simple_oauth/simple_oauth.module
new file mode 100644
index 0000000..c2091ba
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth.module
@@ -0,0 +1,162 @@
+<?php
+
+/**
+ * @file
+ * Contains simple_oauth.module..
+ */
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\consumers\Entity\Consumer;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\Url;
+use Drupal\user\RoleInterface;
+
+/**
+ * Implements hook_cron().
+ */
+function simple_oauth_cron() {
+  /** @var \Drupal\simple_oauth\ExpiredCollector $collector */
+  $collector = \Drupal::service('simple_oauth.expired_collector');
+  // Collect all expired tokens and delete them.
+  $collector->deleteMultipleTokens($collector->collect());
+}
+
+/**
+ * Implements hook_entity_update().
+ */
+function simple_oauth_entity_update(EntityInterface $entity) {
+  /** @var \Drupal\simple_oauth\ExpiredCollector $collector */
+  $collector = \Drupal::service('simple_oauth.expired_collector');
+  // Collect the affected tokens and expire them.
+  if ($entity instanceof AccountInterface) {
+    $collector->deleteMultipleTokens($collector->collectForAccount($entity));
+  }
+  if ($entity instanceof Consumer) {
+    $collector->deleteMultipleTokens($collector->collectForClient($entity));
+  }
+}
+
+/**
+ * Implements hook_entity_base_field_info().
+ */
+function simple_oauth_entity_base_field_info(EntityTypeInterface $entity_type) {
+  $fields = [];
+  if ($entity_type->id() == 'consumer') {
+    $fields['secret'] = BaseFieldDefinition::create('password')
+      ->setLabel(new TranslatableMarkup('Secret'))
+      ->setDescription(new TranslatableMarkup('The secret key of this client (hashed).'));
+
+    $fields['confidential'] = BaseFieldDefinition::create('boolean')
+      ->setLabel(new TranslatableMarkup('Is Confidential?'))
+      ->setDescription(new TranslatableMarkup('A boolean indicating whether the client secret needs to be validated or not.'))
+      ->setDisplayOptions('view', [
+        'label' => 'inline',
+        'type' => 'boolean',
+        'weight' => 3,
+      ])
+      ->setDisplayOptions('form', [
+        'weight' => 3,
+      ])
+      ->setRevisionable(TRUE)
+      ->setTranslatable(TRUE)
+      ->setDefaultValue(TRUE);
+
+    $fields['roles'] = BaseFieldDefinition::create('entity_reference')
+      ->setLabel(new TranslatableMarkup('Scopes'))
+      ->setDescription(new TranslatableMarkup('The roles for this Consumer. OAuth2 scopes are implemented as Drupal roles.'))
+      ->setRevisionable(TRUE)
+      ->setSetting('target_type', 'user_role')
+      ->setSetting('handler', 'default')
+      ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED)
+      ->setTranslatable(FALSE)
+      ->setDisplayOptions('view', [
+        'label' => 'inline',
+        'type' => 'entity_reference_label',
+        'weight' => 5,
+      ])
+      ->setDisplayOptions('form', [
+        'type' => 'options_buttons',
+        'weight' => 5,
+      ]);
+  }
+  return $fields;
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function simple_oauth_form_consumer_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
+  // Add a custom submit behavior.
+  $form['#entity_builders'][] = 'simple_oauth_form_consumer_form_submit';
+
+  $entity_type_manager = \Drupal::service('entity_type.manager');
+  // Remove automatic roles and administrator roles.
+  unset($form['roles']['widget']['#options'][RoleInterface::ANONYMOUS_ID]);
+  unset($form['roles']['widget']['#options'][RoleInterface::AUTHENTICATED_ID]);
+  // Get the admin role.
+  $admin_roles = $entity_type_manager->getStorage('user_role')->getQuery()
+    ->condition('is_admin', TRUE)
+    ->execute();
+  $default_value = reset($admin_roles);
+  unset($form['roles']['widget']['#options'][$default_value]);
+  $recommendation_text = t(
+    'Create a <a href=":url">role</a> for every logical group of permissions you want to make available to a consumer.',
+    [':url' => Url::fromRoute('entity.user_role.collection')->toString()]
+  );
+  $form['roles']['widget']['#description'] .= '<br>' . $recommendation_text;
+  if (empty($form['roles']['widget']['#options'])) {
+    drupal_set_message($recommendation_text, 'error');
+    $form['actions']['#disabled'] = TRUE;
+  }
+
+  $description = t(
+    'Use this field to create a hash of the secret key. This module will never store your consumer key, only a hash of it.'
+  );
+  $form['new_secret'] = [
+    '#type' => 'password',
+    '#title' => t('New Secret'),
+    '#description' => $description,
+  ];
+}
+
+/**
+ * Extra submit handler.
+ *
+ * @param array $form
+ *   The form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ *   The state.
+ */
+function simple_oauth_form_consumer_form_submit($entity_type_id, Consumer $entity, array &$form, FormStateInterface $form_state) {
+  if ($entity_type_id !== 'consumer') {
+    return;
+  }
+  // If the secret was changed, then digest it before saving. If not, then
+  // leave it alone.
+  if ($new_secret = $form_state->getValue('new_secret')) {
+    $entity->get('secret')->value = $new_secret;
+  }
+}
+
+/**
+ * Implements hook_consumers_list_alter().
+ */
+function simple_oauth_consumers_list_alter(&$data, $context) {
+  if ($context['type'] === 'header') {
+    $data['scopes'] = t('Scopes');
+  }
+  else if ($context['type'] === 'row') {
+    $entity = $context['entity'];
+    $data['scopes'] = [
+      'data' => ['#theme' => 'item_list', '#items' => []],
+    ];
+    foreach ($entity->get('roles')->getValue() as $role) {
+      $data['scopes']['data']['#items'][] = $role['target_id'];
+    }
+  }
+}
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth.permissions.yml b/drupal/modules/contrib/simple_oauth/simple_oauth.permissions.yml
new file mode 100644
index 0000000..452ec82
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth.permissions.yml
@@ -0,0 +1,15 @@
+add simple_oauth entities:
+  title: 'Create new Access Token entities'
+
+administer simple_oauth entities:
+  title: 'Administer Access Token entities'
+  description: 'Allow to access the administration form to configure Access Token entities.'
+
+delete own simple_oauth entities:
+  title: 'Delete Access Token entities'
+
+update own simple_oauth entities:
+  title: 'Edit Access Token entities'
+
+view own simple_oauth entities:
+  title: 'View Access Token entities'
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth.routing.yml b/drupal/modules/contrib/simple_oauth/simple_oauth.routing.yml
new file mode 100644
index 0000000..6a3b62a
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth.routing.yml
@@ -0,0 +1,50 @@
+# Oauth2Token routing definition
+entity.oauth2_token.canonical:
+  path: '/admin/config/people/simple_oauth/oauth2_token/{oauth2_token}'
+  defaults:
+    _entity_view: 'oauth2_token'
+    _title: 'Access Token'
+  requirements:
+    _entity_access: 'oauth2_token.view'
+  options:
+    _admin_route: TRUE
+
+entity.oauth2_token.collection:
+  path: '/admin/config/people/simple_oauth/oauth2_token'
+  defaults:
+    _entity_list: 'oauth2_token'
+    _title: 'Access Token list'
+  requirements:
+    _permission: 'administer simple_oauth entities'
+  options:
+    _admin_route: TRUE
+  base_route: system.admin_content
+
+entity.oauth2_token.delete_form:
+  path: '/admin/config/people/simple_oauth/oauth2_token/{oauth2_token}/delete'
+  defaults:
+    _entity_form: oauth2_token.delete
+    _title: 'Delete Access Token'
+  requirements:
+    _entity_access: 'oauth2_token.delete'
+  options:
+    _admin_route: TRUE
+
+oauth2_token.settings:
+  path: '/admin/config/people/simple_oauth'
+  defaults:
+   _form: '\Drupal\simple_oauth\Entity\Form\Oauth2TokenSettingsForm'
+   _title: 'Simple OAuth Settings'
+  requirements:
+    _permission: 'administer simple_oauth entities'
+  options:
+    _admin_route: TRUE
+  base_route: entity.user.admin_form
+
+oauth2_token.token:
+  path: '/oauth/token'
+  defaults:
+    _controller: 'Drupal\simple_oauth\Controller\Oauth2Token::token'
+  methods: [POST]
+  requirements:
+    _access: 'TRUE'
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth.services.yml b/drupal/modules/contrib/simple_oauth/simple_oauth.services.yml
new file mode 100644
index 0000000..6ba64ca
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth.services.yml
@@ -0,0 +1,59 @@
+services:
+  simple_oauth.authentication.simple_oauth:
+    class: Drupal\simple_oauth\Authentication\Provider\SimpleOauthAuthenticationProvider
+    arguments: ['@simple_oauth.server.resource_server', '@entity_type.manager']
+    tags:
+      - { name: authentication_provider, provider_id: oauth2, global: TRUE, priority: 35 }
+  simple_oauth.page_cache_request_policy.disallow_oauth2_token_requests:
+    class: Drupal\simple_oauth\PageCache\DisallowSimpleOauthRequests
+    public: false
+    tags:
+      - { name: page_cache_request_policy }
+
+  simple_oauth.normalizer.oauth2_token:
+    class: Drupal\simple_oauth\Normalizer\TokenEntityNormalizer
+    arguments: ['@entity_type.manager']
+    tags:
+      - { name: normalizer, priority: 21 }
+  simple_oauth.normalizer.oauth2_refresh_token:
+    class: Drupal\simple_oauth\Normalizer\RefreshTokenEntityNormalizer
+    tags:
+      - { name: normalizer, priority: 20 }
+  simple_oauth.server.resource_server:
+    class: Drupal\simple_oauth\Server\ResourceServer
+    arguments:
+      - '@simple_oauth.repositories.access_token'
+      - '@config.factory'
+      - '@psr7.http_message_factory'
+      - '@psr7.http_foundation_factory'
+  simple_oauth.repositories.client:
+    class: Drupal\simple_oauth\Repositories\ClientRepository
+    arguments: ['@entity_type.manager', '@password']
+  simple_oauth.repositories.scope:
+    class: Drupal\simple_oauth\Repositories\ScopeRepository
+    arguments: ['@entity_type.manager']
+  simple_oauth.repositories.user:
+    class: Drupal\simple_oauth\Repositories\UserRepository
+    arguments: ['@user.auth']
+  simple_oauth.repositories.access_token:
+    class: Drupal\simple_oauth\Repositories\AccessTokenRepository
+    arguments: ['@entity_type.manager', '@serializer']
+  simple_oauth.repositories.refresh_token:
+    class: Drupal\simple_oauth\Repositories\RefreshTokenRepository
+    arguments: ['@entity_type.manager', '@serializer']
+  plugin.manager.oauth2_grant.processor:
+    class: Drupal\simple_oauth\Plugin\Oauth2GrantManager
+    parent: default_plugin_manager
+    arguments:
+      - '@simple_oauth.repositories.client'
+      - '@simple_oauth.repositories.scope'
+      - '@simple_oauth.repositories.access_token'
+      - '@simple_oauth.repositories.refresh_token'
+      - '@config.factory'
+  simple_oauth.expired_collector:
+    class: Drupal\simple_oauth\ExpiredCollector
+    arguments: ['@entity_type.manager', '@datetime.time']
+  simple_oauth.http_middleware.basic_auth_swap:
+    class: Drupal\simple_oauth\HttpMiddleware\BasicAuthSwap
+    tags:
+      - { name: http_middleware }
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth_extras/config/install/simple_oauth_extras.settings.yml b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/config/install/simple_oauth_extras.settings.yml
new file mode 100644
index 0000000..ec0c0a3
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/config/install/simple_oauth_extras.settings.yml
@@ -0,0 +1 @@
+use_implicit: false
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth_extras/config/schema/simple_oauth_extras.schema.yml b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/config/schema/simple_oauth_extras.schema.yml
new file mode 100644
index 0000000..5817cee
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/config/schema/simple_oauth_extras.schema.yml
@@ -0,0 +1,8 @@
+simple_oauth_extras.settings:
+  type: config_object
+  label: 'Simple OAuth 3rd Party Settings'
+  mapping:
+    use_implicit:
+      type: boolean
+      label: 'Enable the implicit grant?'
+      description: 'Only use the implicit grant if you understand the security implications of using it.'
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth_extras/simple_oauth_extras.info.yml b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/simple_oauth_extras.info.yml
new file mode 100644
index 0000000..7aa0cd4
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/simple_oauth_extras.info.yml
@@ -0,0 +1,13 @@
+name: Simple OAuth Extras
+type: module
+description: OAuth2 extra access grants.
+# core: 8.x
+package: Authentication
+dependencies:
+  - simple_oauth
+
+# Information added by Drupal.org packaging script on 2017-09-10
+version: '8.x-3.0-beta1'
+core: '8.x'
+project: 'simple_oauth'
+datestamp: 1505028252
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth_extras/simple_oauth_extras.module b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/simple_oauth_extras.module
new file mode 100644
index 0000000..3f9c223
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/simple_oauth_extras.module
@@ -0,0 +1,104 @@
+<?php
+
+/**
+ * @file
+ *
+ * Module implementation file.
+ */
+
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Link;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\Url;
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function simple_oauth_extras_form_oauth2_token_settings_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
+  $form['use_implicit'] = [
+    '#type' => 'checkbox',
+    '#title' => t('Enable the implicit grant?'),
+    '#description' => t('The implicit grant has the potential to be used in an insecure way. Only enable this if you understand the risks. See https://tools.ietf.org/html/rfc6819#section-4.4.2 for more information.'),
+    '#default_value' => \Drupal::config('simple_oauth_extras.settings')->get('use_implicit'),
+  ];
+  $form['#submit'][] = 'simple_oauth_extras_form_oauth2_token_settings_submit';
+}
+
+/**
+ * Form submission handler.
+ *
+ * @param array $form
+ *   An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ *   The current state of the form.
+ */
+function simple_oauth_extras_form_oauth2_token_settings_submit(array &$form, \Drupal\Core\Form\FormStateInterface $form_state) {
+  $settings = \Drupal::configFactory()->getEditable('simple_oauth_extras.settings');
+  $settings->set('use_implicit', $form_state->getValue('use_implicit'));
+  $settings->save();
+}
+
+/**
+ * Implements hook_entity_base_field_info().
+ */
+function simple_oauth_extras_entity_base_field_info(EntityTypeInterface $entity_type) {
+  $fields = [];
+  if ($entity_type->id() == 'consumer') {
+    $fields['redirect'] = BaseFieldDefinition::create('uri')
+      ->setLabel(new TranslatableMarkup('Redirect URI'))
+      ->setDescription(new TranslatableMarkup('The URI this client will redirect to when needed.'))
+      ->setDisplayOptions('view', [
+        'label' => 'inline',
+        'weight' => 4,
+      ])
+      ->setDisplayOptions('form', [
+        'weight' => 4,
+      ])
+      ->setDisplayConfigurable('view', TRUE)
+      ->setTranslatable(TRUE)
+      // URIs are not length limited by RFC 2616, but we can only store 255
+      // characters in our entity DB schema.
+      ->setSetting('max_length', 255);
+
+    $fields['user_id'] = BaseFieldDefinition::create('entity_reference')
+      ->setLabel(new TranslatableMarkup('User'))
+      ->setDescription(new TranslatableMarkup('When no specific user is authenticated Drupal will use this user as the author of all the actions made.'))
+      ->setRevisionable(TRUE)
+      ->setSetting('target_type', 'user')
+      ->setSetting('handler', 'default')
+      ->setTranslatable(FALSE)
+      ->setDisplayOptions('view', [
+        'label' => 'inline',
+        'type' => 'entity_reference_label',
+        'weight' => 1,
+      ])
+      ->setCardinality(1)
+      ->setDisplayOptions('form', [
+        'type' => 'entity_reference_autocomplete',
+        'weight' => 0,
+        'settings' => [
+          'match_operator' => 'CONTAINS',
+          'size' => '60',
+          'autocomplete_type' => 'tags',
+          'placeholder' => '',
+        ],
+      ]);
+  }
+  return $fields;
+}
+
+/**
+ * Implements hook_consumers_list_alter().
+ */
+function simple_oauth_extras_consumers_list_alter(&$data, $context) {
+  if ($context['type'] === 'header') {
+    $data['redirect'] = t('Redirect');
+  }
+  else if ($context['type'] === 'row') {
+    $data['redirect'] = NULL;
+    if ($redirect_url = $context['entity']->get('redirect')->value) {
+      $data['redirect'] = Link::fromTextAndUrl($redirect_url, Url::fromUri($redirect_url));
+    }
+  }
+}
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth_extras/simple_oauth_extras.permissions.yml b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/simple_oauth_extras.permissions.yml
new file mode 100644
index 0000000..49fb84a
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/simple_oauth_extras.permissions.yml
@@ -0,0 +1,3 @@
+grant simple_oauth codes:
+  title: 'Grant OAuth2 codes'
+  description: 'Allow using the AuthCode grant'
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth_extras/simple_oauth_extras.routing.yml b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/simple_oauth_extras.routing.yml
new file mode 100644
index 0000000..d0ba9a0
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/simple_oauth_extras.routing.yml
@@ -0,0 +1,22 @@
+oauth2_token_extras.authorize:
+  path: '/oauth/authorize'
+  defaults:
+   _form: '\Drupal\simple_oauth_extras\Controller\Oauth2AuthorizeForm'
+   _title: 'Grant Access to Client'
+  methods: [GET, POST]
+  requirements:
+    _access: 'TRUE'
+  options:
+    _auth: ['cookie']
+
+oauth2_token.user_debug:
+  path: '/oauth/debug'
+  defaults:
+    _controller: 'Drupal\simple_oauth_extras\Controller\DebugController::debug'
+  methods: [GET]
+  requirements:
+    _access: 'TRUE'
+    _format: 'json'
+  options:
+    _auth: ['oauth2']
+    no_cache: TRUE
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth_extras/simple_oauth_extras.services.yml b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/simple_oauth_extras.services.yml
new file mode 100644
index 0000000..74958d4
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/simple_oauth_extras.services.yml
@@ -0,0 +1,4 @@
+services:
+  simple_oauth_extras.repositories.auth_code:
+    class: Drupal\simple_oauth_extras\Repositories\AuthCodeRepository
+    arguments: ['@entity_type.manager', '@serializer']
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Controller/DebugController.php b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Controller/DebugController.php
new file mode 100644
index 0000000..4d72a73
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Controller/DebugController.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace Drupal\simple_oauth_extras\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\user\PermissionHandlerInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\JsonResponse;
+
+class DebugController extends ControllerBase {
+
+  /**
+   * @var \Drupal\user\PermissionHandlerInterface
+   */
+  protected $userPermissions;
+
+  /**
+   * Oauth2Token constructor.
+   *
+   * @param \Drupal\user\PermissionHandlerInterface $user_permissions
+   */
+  public function __construct(PermissionHandlerInterface $user_permissions) {
+    $this->userPermissions = $user_permissions;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('user.permissions')
+    );
+  }
+
+  /**
+   * Processes a GET request.
+   */
+  public function debug(ServerRequestInterface $request) {
+    $user = $this->currentUser();
+    $permissions_list = $this->userPermissions->getPermissions();
+    $permission_info = [];
+    // Loop over all the permissions and check if the user has access or not.
+    foreach ($permissions_list as $permission_id => $permission) {
+      $permission_info[$permission_id] = [
+        'title' => $permission['title'],
+        'access' => $user->hasPermission($permission_id),
+      ];
+      if (!empty($permission['description'])) {
+        $permission_info['description'] = $permission['description'];
+      }
+    }
+
+    return new JsonResponse([
+      'token' => str_replace('Bearer ', '', $request->getHeader('Authorization')),
+      'id' => $user->id(),
+      'roles' => $user->getRoles(),
+      'permissions' => $permission_info,
+    ]);
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Controller/Oauth2AuthorizeForm.php b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Controller/Oauth2AuthorizeForm.php
new file mode 100644
index 0000000..ef2595a
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Controller/Oauth2AuthorizeForm.php
@@ -0,0 +1,218 @@
+<?php
+
+
+namespace Drupal\simple_oauth_extras\Controller;
+
+use Drupal\Component\Utility\UrlHelper;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Routing\TrustedRedirectResponse;
+use Drupal\Core\Url;
+use Drupal\simple_oauth\Entities\UserEntity;
+use Drupal\simple_oauth\Plugin\Oauth2GrantManagerInterface;
+use GuzzleHttp\Psr7\Response;
+use League\OAuth2\Server\Exception\OAuthServerException;
+use Symfony\Bridge\PsrHttpMessage\HttpFoundationFactoryInterface;
+use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+class Oauth2AuthorizeForm extends FormBase {
+
+  /**
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * @var \Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface
+   */
+  protected $messageFactory;
+
+  /**
+   * @var \Symfony\Bridge\PsrHttpMessage\HttpFoundationFactoryInterface
+   */
+  protected $foundationFactory;
+
+  /**
+   * @var \League\OAuth2\Server\AuthorizationServer
+   */
+  protected $server;
+
+  /**
+   * @var \Drupal\simple_oauth\Plugin\Oauth2GrantManagerInterface
+   */
+  protected $grantManager;
+
+  /**
+   * Oauth2AuthorizeForm constructor.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   * @param \Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface $message_factory
+   * @param \Symfony\Bridge\PsrHttpMessage\HttpFoundationFactoryInterface $foundation_factory
+   * @param \Drupal\simple_oauth\Plugin\Oauth2GrantManagerInterface $grant_manager
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, HttpMessageFactoryInterface $message_factory, HttpFoundationFactoryInterface $foundation_factory, Oauth2GrantManagerInterface $grant_manager) {
+    $this->entityTypeManager = $entity_type_manager;
+    $this->messageFactory = $message_factory;
+    $this->foundationFactory = $foundation_factory;
+    $this->grantManager = $grant_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity_type.manager'),
+      $container->get('psr7.http_message_factory'),
+      $container->get('psr7.http_foundation_factory'),
+      $container->get('plugin.manager.oauth2_grant.processor')
+    );
+  }
+
+  /**
+   * Returns a unique string identifying the form.
+   *
+   * @return string
+   *   The unique string identifying the form.
+   */
+  public function getFormId() {
+    return 'simple_oauth_authorize_form';
+  }
+
+  /**
+   * Form constructor.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return array
+   *   The form structure.
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+      if (!$this->currentUser()->isAuthenticated()) {
+      $form['redirect_params'] = ['#type' => 'hidden', '#value' => $this->getRequest()->getQueryString()];
+      $form['description'] = [
+        '#type' => 'html_tag',
+        '#tag' => 'p',
+        '#value' => $this->t('An external client application is requesting access to your data in this site. Please log in first to authorize the operation.'),
+      ];
+      $form['submit'] = ['#type' => 'submit', '#value' => $this->t('Login')];
+      return $form;
+    }
+    $request = $this->getRequest();
+    if ($request->get('response_type') == 'code') {
+      $grant_type = 'code';
+    }
+    elseif ($request->get('response_type') == 'token') {
+      $grant_type = 'implicit';
+    }
+    else {
+      $grant_type = NULL;
+    }
+    $this->server = $this
+      ->grantManager
+      ->getAuthorizationServer($grant_type);
+
+    // Transform the HTTP foundation request object into a PSR-7 object. The
+    // OAuth library expects a PSR-7 request.
+    $psr7_request = $this->messageFactory->createRequest($request);
+    // Validate the HTTP request and return an AuthorizationRequest object.
+    // The auth request object can be serialized into a user's session.
+    $auth_request = $this->server->validateAuthorizationRequest($psr7_request);
+
+    // Store the auth request temporarily.
+    $form_state->set('auth_request', $auth_request);
+
+    $manager = $this->entityTypeManager;
+    $form = [
+      '#type' => 'container',
+    ];
+
+    $client_uuid = $request->get('client_id');
+    $client_drupal_entities = $manager->getStorage('consumer')->loadByProperties([
+      'uuid' => $client_uuid,
+    ]);
+    if (empty($client_drupal_entities)) {
+      throw OAuthServerException::invalidClient();
+    }
+    $client_drupal_entity = reset($client_drupal_entities);
+
+    // Gather all the role ids.
+    $scope_ids = array_merge(
+      explode(' ', $request->get('scope')),
+      array_map(function ($item) {
+        return $item['target_id'];
+      }, $client_drupal_entity->get('roles')->getValue())
+    );
+    $user_roles = $manager->getStorage('user_role')->loadMultiple($scope_ids);
+    $form['client'] = $manager->getViewBuilder('consumer')->view($client_drupal_entity);
+    $client_drupal_entity->addCacheableDependency($form['client']);
+    $form['scopes'] = [
+      '#title' => $this->t('Permissions'),
+      '#theme' => 'item_list',
+      '#items' => [],
+    ];
+    foreach ($user_roles as $user_role) {
+      $user_role->addCacheableDependency($form['scopes']);
+      $form['scopes']['#items'][] = $user_role->label();
+    }
+
+    $form['redirect_uri'] = [
+      '#type' => 'hidden',
+      '#value' => $request->get('redirect_uri') ?
+        $request->get('redirect_uri') :
+        $client_drupal_entity->get('redirect')->value,
+    ];
+    $form['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Grant'),
+    ];
+
+    return $form;
+  }
+
+  /**
+   * Form submission handler.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    if ($auth_request = $form_state->get('auth_request')) {
+      // Once the user has logged in set the user on the AuthorizationRequest.
+      $user_entity = new UserEntity();
+      $user_entity->setIdentifier($this->currentUser()->id());
+      $auth_request->setUser($user_entity);
+      // Once the user has approved or denied the client update the status
+      // (true = approved, false = denied).
+      $can_grant_codes = $this->currentUser()->hasPermission('grant simple_oauth codes');
+      $auth_request->setAuthorizationApproved((bool) $form_state->getValue('submit') && $can_grant_codes);
+      // Return the HTTP redirect response.
+      $response = $this->server->completeAuthorizationRequest($auth_request, new Response());
+      // Get the location and return a secure redirect response.
+      $redirect_response = TrustedRedirectResponse::create(
+        $response->getHeaderLine('location'),
+        $response->getStatusCode(),
+        $response->getHeaders()
+      );
+      $form_state->setResponse($redirect_response);
+    }
+    elseif ($params = $form_state->getValue('redirect_params')) {
+      $url = Url::fromRoute('user.login');
+      $destination = Url::fromRoute('oauth2_token_extras.authorize', [], [
+        'query' => UrlHelper::parse('/?' . $params)['query'],
+      ]);
+      $url->setOption('query', [
+        'destination' => $destination->toString(),
+      ]);
+      $form_state->setRedirectUrl($url);
+    }
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Entities/AuthCodeEntity.php b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Entities/AuthCodeEntity.php
new file mode 100644
index 0000000..989357c
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Entities/AuthCodeEntity.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Drupal\simple_oauth_extras\Entities;
+
+use League\OAuth2\Server\Entities\AuthCodeEntityInterface;
+use League\OAuth2\Server\Entities\Traits\AuthCodeTrait;
+use League\OAuth2\Server\Entities\Traits\EntityTrait;
+use League\OAuth2\Server\Entities\Traits\TokenEntityTrait;
+
+class AuthCodeEntity implements AuthCodeEntityInterface {
+
+  use EntityTrait, TokenEntityTrait, AuthCodeTrait;
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Plugin/Oauth2Grant/AuthorizationCode.php b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Plugin/Oauth2Grant/AuthorizationCode.php
new file mode 100644
index 0000000..7fd56a6
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Plugin/Oauth2Grant/AuthorizationCode.php
@@ -0,0 +1,73 @@
+<?php
+
+
+namespace Drupal\simple_oauth_extras\Plugin\Oauth2Grant;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\simple_oauth\Plugin\Oauth2GrantBase;
+use League\OAuth2\Server\Grant\AuthCodeGrant;
+use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface;
+use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * @Oauth2Grant(
+ *   id = "authorization_code",
+ *   label = @Translation("Authorization Code")
+ * )
+ */
+class AuthorizationCode extends Oauth2GrantBase {
+
+  /**
+   * @var \League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface
+   */
+  protected $authCodeRepository;
+
+  /**
+   * @var \League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface
+   */
+  protected $refreshTokenRepository;
+
+  /**
+   * @var \DateTime
+   */
+  protected $authCodeExpiration;
+
+  /**
+   * Class constructor.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, AuthCodeRepositoryInterface $auth_code_repository, RefreshTokenRepositoryInterface $refresh_token_repository, ConfigFactoryInterface $config_factory) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $settings = $config_factory->get('simple_oauth.settings');
+    $this->authCodeRepository = $auth_code_repository;
+    $this->refreshTokenRepository = $refresh_token_repository;
+    // TODO: Make this configurable and not just the same as the access toke expiration.
+    $this->authCodeExpiration = new \DateInterval(sprintf('PT%dS', $settings->get('access_token_expiration')));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('simple_oauth_extras.repositories.auth_code'),
+      $container->get('simple_oauth.repositories.refresh_token'),
+      $container->get('config.factory')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getGrantType() {
+    return new AuthCodeGrant(
+      $this->authCodeRepository,
+      $this->refreshTokenRepository,
+      $this->authCodeExpiration
+    );
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Plugin/Oauth2Grant/ClientCredentials.php b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Plugin/Oauth2Grant/ClientCredentials.php
new file mode 100644
index 0000000..110e715
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Plugin/Oauth2Grant/ClientCredentials.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Drupal\simple_oauth_extras\Plugin\Oauth2Grant;
+
+use Drupal\simple_oauth\Plugin\Oauth2GrantBase;
+use League\OAuth2\Server\Grant\ClientCredentialsGrant;
+use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * @Oauth2Grant(
+ *   id = "client_credentials",
+ *   label = @Translation("Client Credentials")
+ * )
+ */
+class ClientCredentials extends Oauth2GrantBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getGrantType() {
+    return new ClientCredentialsGrant();
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Plugin/Oauth2Grant/Code.php b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Plugin/Oauth2Grant/Code.php
new file mode 100644
index 0000000..c3f8d5d
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Plugin/Oauth2Grant/Code.php
@@ -0,0 +1,12 @@
+<?php
+
+
+namespace Drupal\simple_oauth_extras\Plugin\Oauth2Grant;
+
+/**
+ * @Oauth2Grant(
+ *   id = "code",
+ *   label = @Translation("Code")
+ * )
+ */
+class Code extends AuthorizationCode {}
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Plugin/Oauth2Grant/Implicit.php b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Plugin/Oauth2Grant/Implicit.php
new file mode 100644
index 0000000..16ff532
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Plugin/Oauth2Grant/Implicit.php
@@ -0,0 +1,58 @@
+<?php
+
+
+namespace Drupal\simple_oauth_extras\Plugin\Oauth2Grant;
+
+use Drupal\Component\Plugin\Exception\PluginNotFoundException;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\simple_oauth\Plugin\Oauth2GrantBase;
+use League\OAuth2\Server\Grant\ImplicitGrant;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * @Oauth2Grant(
+ *   id = "implicit",
+ *   label = @Translation("Implicit")
+ * )
+ */
+class Implicit extends Oauth2GrantBase {
+
+  /**
+   * @var \DateTime
+   */
+  protected $expiration;
+
+  /**
+   * Class constructor.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, ConfigFactoryInterface $config_factory) {
+    // If the implicit grant is not enabled, then bail.
+    $enabled = $config_factory->get('simple_oauth_extras.settings')->get('use_implicit');
+    if (!$enabled) {
+      throw new PluginNotFoundException('implicit');
+    }
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $settings = $config_factory->get('simple_oauth.settings');
+    $this->expiration = new \DateInterval(sprintf('PT%dS', $settings->get('access_token_expiration')));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('config.factory')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getGrantType() {
+    return new ImplicitGrant($this->expiration);
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Plugin/Oauth2Grant/RefreshToken.php b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Plugin/Oauth2Grant/RefreshToken.php
new file mode 100644
index 0000000..3c9dc49
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Plugin/Oauth2Grant/RefreshToken.php
@@ -0,0 +1,64 @@
+<?php
+
+
+namespace Drupal\simple_oauth_extras\Plugin\Oauth2Grant;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\simple_oauth\Plugin\Oauth2GrantBase;
+use League\OAuth2\Server\Grant\RefreshTokenGrant;
+use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * @Oauth2Grant(
+ *   id = "refresh_token",
+ *   label = @Translation("Refresh Token")
+ * )
+ */
+class RefreshToken extends Oauth2GrantBase {
+
+  /**
+   * @var \League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface
+   */
+  protected $refreshTokenRepository;
+
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * Class constructor.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, RefreshTokenRepositoryInterface $refresh_token_repository, ConfigFactoryInterface $config_factory) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->refreshTokenRepository = $refresh_token_repository;
+    $this->configFactory = $config_factory;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('simple_oauth.repositories.refresh_token'),
+      $container->get('config.factory')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getGrantType() {
+    $grant = new RefreshTokenGrant($this->refreshTokenRepository);
+    $settings = $this->configFactory->get('simple_oauth.settings');
+    $grant->setRefreshTokenTTL(new \DateInterval(sprintf('PT%dS', $settings->get('refresh_token_expiration'))));
+    return $grant;
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Repositories/AuthCodeRepository.php b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Repositories/AuthCodeRepository.php
new file mode 100644
index 0000000..6acb591
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/src/Repositories/AuthCodeRepository.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Drupal\simple_oauth_extras\Repositories;
+
+use Drupal\simple_oauth\Repositories\RevocableTokenRepositoryTrait;
+use League\OAuth2\Server\Entities\AuthCodeEntityInterface;
+use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface;
+
+class AuthCodeRepository implements AuthCodeRepositoryInterface {
+
+  use RevocableTokenRepositoryTrait;
+
+  protected static $bundle_id = 'auth_code';
+  protected static $entity_class = 'Drupal\simple_oauth_extras\Entities\AuthCodeEntity';
+  protected static $entity_interface = 'League\OAuth2\Server\Entities\AuthCodeEntityInterface';
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getNewAuthCode() {
+    return $this->getNew();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function persistNewAuthCode(AuthCodeEntityInterface $auth_code_entity) {
+    $this->persistNew($auth_code_entity);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function revokeAuthCode($code_id) {
+    $this->revoke($code_id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isAuthCodeRevoked($code_id) {
+    return $this->isRevoked($code_id);
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth_extras/tests/simple_oauth_extras_test/simple_oauth_extras_test.info.yml b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/tests/simple_oauth_extras_test/simple_oauth_extras_test.info.yml
new file mode 100644
index 0000000..88e684d
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/tests/simple_oauth_extras_test/simple_oauth_extras_test.info.yml
@@ -0,0 +1,14 @@
+name: Simple OAuth Extras Test
+type: module
+description: Test module for Simple OAuth Extras
+# core: 8.x
+package: Testing
+hidden: true
+dependencies:
+  - simple_oauth_extras
+
+# Information added by Drupal.org packaging script on 2017-09-10
+version: '8.x-3.0-beta1'
+core: '8.x'
+project: 'simple_oauth'
+datestamp: 1505028252
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth_extras/tests/simple_oauth_extras_test/simple_oauth_extras_test.routing.yml b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/tests/simple_oauth_extras_test/simple_oauth_extras_test.routing.yml
new file mode 100644
index 0000000..6b4c5fa
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/tests/simple_oauth_extras_test/simple_oauth_extras_test.routing.yml
@@ -0,0 +1,8 @@
+oauth2_token_extras.test_token:
+  path: '/oauth/test'
+  defaults:
+   _controller: '\Drupal\simple_oauth_extras_test\Controller\RedirectDebug::token'
+   _title: 'Test Route'
+  methods: [GET]
+  requirements:
+    _access: 'TRUE'
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth_extras/tests/simple_oauth_extras_test/src/Controller/RedirectDebug.php b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/tests/simple_oauth_extras_test/src/Controller/RedirectDebug.php
new file mode 100644
index 0000000..5ac7c94
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/tests/simple_oauth_extras_test/src/Controller/RedirectDebug.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace Drupal\simple_oauth_extras_test\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\HttpFoundation\Request;
+
+class RedirectDebug extends ControllerBase {
+
+  /**
+   * Debug the token response for the implicit grant.
+   */
+  public function token(Request $request) {
+    return new JsonResponse($request->getRequestUri());
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth_extras/tests/src/Functional/AuthCodeFunctionalTest.php b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/tests/src/Functional/AuthCodeFunctionalTest.php
new file mode 100644
index 0000000..e15e6f8
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/tests/src/Functional/AuthCodeFunctionalTest.php
@@ -0,0 +1,102 @@
+<?php
+
+namespace Drupal\Tests\simple_oauth_extras\Functional;
+
+use Drupal\Core\Url;
+use Drupal\Tests\simple_oauth\Functional\TokenBearerFunctionalTestBase;
+use Drupal\user\Entity\Role;
+use Drupal\user\RoleInterface;
+
+/**
+ * @group simple_oauth_extras
+ */
+class AuthCodeFunctionalTest extends TokenBearerFunctionalTestBase {
+
+  /**
+   * @var \Drupal\Core\Url
+   */
+  protected $authorizeUrl;
+
+  /**
+   * @var string
+   */
+  protected $redirectUri;
+
+  public static $modules = [
+    'simple_oauth_extras',
+    'simple_oauth_extras_test',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->redirectUri = Url::fromRoute('oauth2_token_extras.test_token', [], [
+      'absolute' => TRUE,
+    ])->toString();
+    $this->client->set('redirect', $this->redirectUri);
+    $this->client->set('description', $this->getRandomGenerator()->paragraphs());
+    $this->client->save();
+    $this->authorizeUrl = Url::fromRoute('oauth2_token_extras.authorize');
+    $this->grantPermissions(Role::load(RoleInterface::AUTHENTICATED_ID), [
+      'grant simple_oauth codes',
+    ]);
+  }
+
+  /**
+   * Test the valid AuthCode grant.
+   */
+  public function testAuthCodeGrant() {
+    $valid_params = [
+      'response_type' => 'code',
+      'client_id' => $this->client->uuid(),
+      'client_secret' => $this->clientSecret,
+    ];
+    // 1. Anonymous request invites the user to log in.
+    $this->drupalGet($this->authorizeUrl->toString(), [
+      'query' => $valid_params,
+    ]);
+    $assert_session = $this->assertSession();
+    $assert_session->buttonExists(t('Login'));
+    $assert_session->responseContains(t('An external client application is requesting access'));
+
+    // 2. Log the user in and try again.
+    $this->drupalLogin($this->user);
+    $this->drupalGet($this->authorizeUrl->toString(), [
+      'query' => $valid_params,
+    ]);
+    $assert_session = $this->assertSession();
+    $assert_session->statusCodeEquals(200);
+    $assert_session->titleEquals('Grant Access to Client | Drupal');
+    $assert_session->buttonExists('Grant');
+    $assert_session->responseContains('Permissions');
+
+    // 3. Grant access by submitting the form and get the token back.
+    $this->drupalPostForm($this->authorizeUrl, [], 'Grant', [
+      'query' => $valid_params,
+    ]);
+    $assert_session = $this->assertSession();
+    $session = $this->getSession();
+    $assert_session->statusCodeEquals(200);
+    $parsed_url = parse_url($session->getCurrentUrl());
+    $parsed_query = \GuzzleHttp\Psr7\parse_query($parsed_url['query']);
+    $this->assertArrayHasKey('code', $parsed_query);
+    // Store the code for the second part of the flow.
+    $code = $parsed_query['code'];
+
+    // 4. Send the code to get the access token.
+    $valid_payload = [
+      'grant_type' => 'authorization_code',
+      'client_id' => $this->client->uuid(),
+      'client_secret' => $this->clientSecret,
+      'code' => $code,
+      'scope' => $this->scope,
+    ];
+    $response = $this->request('POST', $this->url, [
+      'form_params' => $valid_payload,
+    ]);
+    $this->assertValidTokenResponse($response, TRUE);
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth_extras/tests/src/Functional/ClientCredentialsFunctionalTest.php b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/tests/src/Functional/ClientCredentialsFunctionalTest.php
new file mode 100644
index 0000000..d99fc76
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/tests/src/Functional/ClientCredentialsFunctionalTest.php
@@ -0,0 +1,117 @@
+<?php
+
+namespace Drupal\Tests\simple_oauth_extras\Functional;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Tests\simple_oauth\Functional\TokenBearerFunctionalTestBase;
+
+/**
+ * @group simple_oauth_extras
+ */
+class ClientCredentialsFunctionalTest extends TokenBearerFunctionalTestBase {
+
+  public static $modules = [
+    'simple_oauth_extras',
+  ];
+
+  /**
+   * Test the valid ClientCredentials grant.
+   */
+  public function testClientCredentialsGrant() {
+    // 1. Test the valid response.
+    $valid_payload = [
+      'grant_type' => 'client_credentials',
+      'client_id' => $this->client->uuid(),
+      'client_secret' => $this->clientSecret,
+      'scope' => $this->scope,
+    ];
+    $response = $this->request('POST', $this->url, [
+      'form_params' => $valid_payload,
+    ]);
+    $this->assertValidTokenResponse($response, FALSE);
+
+    // 2. Test the valid without scopes.
+    $payload_no_scope = $valid_payload;
+    unset($payload_no_scope['scope']);
+    $response = $this->request('POST', $this->url, [
+      'form_params' => $payload_no_scope,
+    ]);
+    $this->assertValidTokenResponse($response, FALSE);
+
+  }
+
+  /**
+   * Test invalid ClientCredentials grant.
+   */
+  public function testMissingClientCredentialsGrant() {
+    $valid_payload = [
+      'grant_type' => 'client_credentials',
+      'client_id' => $this->client->uuid(),
+      'client_secret' => $this->clientSecret,
+      'scope' => $this->scope,
+    ];
+
+    $data = [
+      'grant_type' => [
+        'error' => 'invalid_grant',
+        'code' => 400,
+      ],
+      'client_id' => [
+        'error' => 'invalid_request',
+        'code' => 400,
+      ],
+      'client_secret' => [
+        'error' => 'invalid_client',
+        'code' => 401,
+      ],
+    ];
+    foreach ($data as $key => $value) {
+      $invalid_payload = $valid_payload;
+      unset($invalid_payload[$key]);
+      $response = $this->request('POST', $this->url, [
+        'form_params' => $invalid_payload,
+      ]);
+      $parsed_response = Json::decode($response->getBody()->getContents());
+      $this->assertSame($value['error'], $parsed_response['error'], sprintf('Correct error code %s for %s.', $value['error'], $key));
+      $this->assertSame($value['code'], $response->getStatusCode(), sprintf('Correct status code %d for %s.', $value['code'], $key));
+    }
+  }
+
+  /**
+   * Test invalid ClientCredentials grant.
+   */
+  public function testInvalidClientCredentialsGrant() {
+    $valid_payload = [
+      'grant_type' => 'client_credentials',
+      'client_id' => $this->client->uuid(),
+      'client_secret' => $this->clientSecret,
+      'scope' => $this->scope,
+    ];
+
+    $data = [
+      'grant_type' => [
+        'error' => 'invalid_grant',
+        'code' => 400,
+      ],
+      'client_id' => [
+        'error' => 'invalid_client',
+        'code' => 401,
+      ],
+      'client_secret' => [
+        'error' => 'invalid_client',
+        'code' => 401,
+      ],
+    ];
+    foreach ($data as $key => $value) {
+      $invalid_payload = $valid_payload;
+      $invalid_payload[$key] = $this->getRandomGenerator()->string();
+      $response = $this->request('POST', $this->url, [
+        'form_params' => $invalid_payload,
+      ]);
+      $parsed_response = Json::decode($response->getBody()->getContents());
+      $this->assertSame($value['error'], $parsed_response['error'], sprintf('Correct error code %s for %s.', $value['error'], $key));
+      $this->assertSame($value['code'], $response->getStatusCode(), sprintf('Correct status code %d for %s.', $value['code'], $key));
+    }
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth_extras/tests/src/Functional/ImplicitFunctionalTest.php b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/tests/src/Functional/ImplicitFunctionalTest.php
new file mode 100644
index 0000000..ecd03f2
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/tests/src/Functional/ImplicitFunctionalTest.php
@@ -0,0 +1,92 @@
+<?php
+
+namespace Drupal\Tests\simple_oauth_extras\Functional;
+
+use Drupal\Core\Url;
+use Drupal\Tests\simple_oauth\Functional\TokenBearerFunctionalTestBase;
+use Drupal\user\Entity\Role;
+use Drupal\user\RoleInterface;
+
+/**
+ * @group simple_oauth_extras
+ */
+class ImplicitFunctionalTest extends TokenBearerFunctionalTestBase {
+
+  /**
+   * @var \Drupal\Core\Url
+   */
+  protected $authorizeUrl;
+
+  /**
+   * @var string
+   */
+  protected $redirectUri;
+
+  public static $modules = [
+    'simple_oauth_extras',
+    'simple_oauth_extras_test',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->redirectUri = Url::fromRoute('oauth2_token_extras.test_token', [], [
+      'absolute' => TRUE,
+    ])->toString();
+    $this->client->set('redirect', $this->redirectUri);
+    $this->client->save();
+    $this->authorizeUrl = Url::fromRoute('oauth2_token_extras.authorize');
+    $this->grantPermissions(Role::load(RoleInterface::AUTHENTICATED_ID), [
+      'grant simple_oauth codes',
+    ]);
+  }
+
+  /**
+   * Test the valid Implicit grant.
+   */
+  public function testImplicitGrant() {
+    $valid_params = [
+      'response_type' => 'token',
+      'client_id' => $this->client->uuid(),
+      'client_secret' => $this->clientSecret,
+    ];
+    // 1. Anonymous request invites the user to log in.
+    $this->drupalGet($this->authorizeUrl->toString(), [
+      'query' => $valid_params,
+    ]);
+    $assert_session = $this->assertSession();
+    $assert_session->buttonExists(t('Login'));
+    $assert_session->responseContains(t('An external client application is requesting access'));
+
+    // 2. Log the user in and try again.
+    $this->drupalLogin($this->user);
+    $this->drupalGet($this->authorizeUrl->toString(), [
+      'query' => $valid_params,
+    ]);
+    $assert_session = $this->assertSession();
+    $assert_session->statusCodeEquals(500);
+    $this
+      ->config('simple_oauth_extras.settings')
+      ->set('use_implicit', TRUE)
+      ->save();
+    $this->drupalGet($this->authorizeUrl->toString(), [
+      'query' => $valid_params,
+    ]);
+    $assert_session = $this->assertSession();
+    $assert_session->statusCodeEquals(200);
+    $assert_session->titleEquals('Grant Access to Client | Drupal');
+    $assert_session->buttonExists('Grant');
+    $assert_session->responseContains('Permissions');
+
+    // 3. Grant access by submitting the form and get the token back.
+    $this->drupalPostForm($this->authorizeUrl, [], 'Grant', [
+      'query' => $valid_params,
+    ]);
+    $assert_session = $this->assertSession();
+    $assert_session->statusCodeEquals(200);
+    $assert_session->addressMatches('/\/oauth\/test#access_token=.*&token_type=bearer&expires_in=\d*/');
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth_extras/tests/src/Functional/RefreshFunctionalTest.php b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/tests/src/Functional/RefreshFunctionalTest.php
new file mode 100644
index 0000000..bb46c99
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/tests/src/Functional/RefreshFunctionalTest.php
@@ -0,0 +1,179 @@
+<?php
+
+namespace Drupal\Tests\simple_oauth_extras\Functional;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Tests\simple_oauth\Functional\TokenBearerFunctionalTestBase;
+use League\OAuth2\Server\CryptTrait;
+
+/**
+ * @group simple_oauth_extras
+ */
+class RefreshFunctionalTest extends TokenBearerFunctionalTestBase {
+
+  use CryptTrait;
+
+  public static $modules = [
+    'simple_oauth_extras',
+  ];
+  /**
+   * @var string
+   */
+  protected $refreshToken;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->scope = 'authenticated';
+    $valid_payload = [
+      'grant_type' => 'password',
+      'client_id' => $this->client->uuid(),
+      'client_secret' => $this->clientSecret,
+      'username' => $this->user->getAccountName(),
+      'password' => $this->user->pass_raw,
+      'scope' => $this->scope,
+    ];
+    $response = $this->request('POST', $this->url, [
+      'form_params' => $valid_payload,
+    ]);
+    $body = $response->getBody()->getContents();
+    $parsed_response = Json::decode($body);
+    $this->refreshToken = $parsed_response['refresh_token'];
+  }
+
+  /**
+   * Test the valid Refresh grant.
+   */
+  public function testRefreshGrant() {
+    // 1. Test the valid response.
+    $valid_payload = [
+      'grant_type' => 'refresh_token',
+      'client_id' => $this->client->uuid(),
+      'client_secret' => $this->clientSecret,
+      'refresh_token' => $this->refreshToken,
+      'scope' => $this->scope,
+    ];
+    $response = $this->request('POST', $this->url, [
+      'form_params' => $valid_payload,
+    ]);
+    $this->assertValidTokenResponse($response, TRUE);
+
+    // 2. Test the valid without scopes.
+    // We need to use the new refresh token, the old one is revoked.
+    $response->getBody()->rewind();
+    $parsed_response = Json::decode($response->getBody()->getContents());
+    $valid_payload = [
+      'grant_type' => 'refresh_token',
+      'client_id' => $this->client->uuid(),
+      'client_secret' => $this->clientSecret,
+      'refresh_token' => $parsed_response['refresh_token'],
+      'scope' => $this->scope,
+    ];
+    $response = $this->request('POST', $this->url, [
+      'form_params' => $valid_payload,
+    ]);
+    $this->assertValidTokenResponse($response, TRUE);
+
+    // 3. Test that the token token was revoked.
+    $valid_payload = [
+      'grant_type' => 'refresh_token',
+      'client_id' => $this->client->uuid(),
+      'client_secret' => $this->clientSecret,
+      'refresh_token' => $this->refreshToken,
+    ];
+    $response = $this->request('POST', $this->url, [
+      'form_params' => $valid_payload,
+    ]);
+    $parsed_response = Json::decode($response->getBody()->getContents());
+    $this->assertSame(401, $response->getStatusCode());
+    $this->assertSame('invalid_request', $parsed_response['error']);
+  }
+
+  /**
+   * Test invalid Refresh grant.
+   */
+  public function testMissingRefreshGrant() {
+    $valid_payload = [
+      'grant_type' => 'refresh_token',
+      'client_id' => $this->client->uuid(),
+      'client_secret' => $this->clientSecret,
+      'refresh_token' => $this->refreshToken,
+      'scope' => $this->scope,
+    ];
+
+    $data = [
+      'grant_type' => [
+        'error' => 'invalid_grant',
+        'code' => 400,
+      ],
+      'client_id' => [
+        'error' => 'invalid_request',
+        'code' => 400,
+      ],
+      'client_secret' => [
+        'error' => 'invalid_client',
+        'code' => 401,
+      ],
+      'refresh_token' => [
+        'error' => 'invalid_request',
+        'code' => 400,
+      ],
+    ];
+    foreach ($data as $key => $value) {
+      $invalid_payload = $valid_payload;
+      unset($invalid_payload[$key]);
+      $response = $this->request('POST', $this->url, [
+        'form_params' => $invalid_payload,
+      ]);
+      $parsed_response = Json::decode($response->getBody()->getContents());
+      $this->assertSame($value['error'], $parsed_response['error'], sprintf('Correct error code %s for %s.', $value['error'], $key));
+      $this->assertSame($value['code'], $response->getStatusCode(), sprintf('Correct status code %d for %s.', $value['code'], $key));
+    }
+  }
+
+  /**
+   * Test invalid Refresh grant.
+   */
+  public function testInvalidRefreshGrant() {
+    $valid_payload = [
+      'grant_type' => 'refresh_token',
+      'client_id' => $this->client->uuid(),
+      'client_secret' => $this->clientSecret,
+      'refresh_token' => $this->refreshToken,
+      'scope' => $this->scope,
+    ];
+
+    $data = [
+      'grant_type' => [
+        'error' => 'invalid_grant',
+        'code' => 400,
+      ],
+      'client_id' => [
+        'error' => 'invalid_client',
+        'code' => 401,
+      ],
+      'client_secret' => [
+        'error' => 'invalid_client',
+        'code' => 401,
+      ],
+      'refresh_token' => [
+        'error' => 'invalid_request',
+        'code' => 401,
+      ],
+    ];
+    foreach ($data as $key => $value) {
+      $invalid_payload = $valid_payload;
+      $invalid_payload[$key] = $this->getRandomGenerator()->string();
+      $response = $this->request('POST', $this->url, [
+        'form_params' => $invalid_payload,
+      ]);
+      $parsed_response = Json::decode($response->getBody()->getContents());
+      $this->assertSame($value['error'], $parsed_response['error'], sprintf('Correct error code %s for %s.', $value['error'], $key));
+      $this->assertSame($value['code'], $response->getStatusCode(), sprintf('Correct status code %d for %s.', $value['code'], $key));
+    }
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/simple_oauth_extras/tests/src/Functional/RolesNegotiationFunctionalTest.php b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/tests/src/Functional/RolesNegotiationFunctionalTest.php
new file mode 100644
index 0000000..d9aaa0a
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/simple_oauth_extras/tests/src/Functional/RolesNegotiationFunctionalTest.php
@@ -0,0 +1,320 @@
+<?php
+
+namespace Drupal\Tests\simple_oauth_extras\Functional;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Core\Url;
+use Drupal\consumers\Entity\Consumer;
+use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\simple_oauth\Functional\RequestHelperTrait;
+use Drupal\user\Entity\Role;
+
+/**
+ * @group simple_oauth_extras
+ */
+class RolesNegotiationFunctionalTest extends BrowserTestBase {
+
+  use RequestHelperTrait;
+
+  public static $modules = [
+    'image',
+    'simple_oauth',
+    'simple_oauth_extras',
+    'text',
+    'user',
+  ];
+
+  /**
+   * @var \Drupal\Core\Url
+   */
+  protected $url;
+
+  /**
+   * @var \Drupal\Core\Url
+   */
+  protected $tokenTestUrl;
+
+  /**
+   * @var \Drupal\consumers\Entity\Consumer
+   */
+  protected $client;
+
+  /**
+   * @var \Drupal\user\UserInterface
+   */
+  protected $user;
+
+
+  /**
+   * @var \GuzzleHttp\ClientInterface
+   */
+  protected $httpClient;
+
+  /**
+   * @var string
+   */
+  protected $privateKeyPath;
+
+  /**
+   * @var string
+   */
+  protected $publicKeyPath;
+
+  /**
+   * @var string
+   */
+  protected $clientSecret;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+    $this->htmlOutputEnabled = FALSE;
+    $this->tokenTestUrl = Url::fromRoute('oauth2_token.user_debug');
+    $this->url = Url::fromRoute('oauth2_token.token');
+    $this->user = $this->drupalCreateUser();
+    // Set up a HTTP client that accepts relative URLs.
+    $this->httpClient = $this->container->get('http_client_factory')
+      ->fromOptions(['base_uri' => $this->baseUrl]);
+    $this->clientSecret = $this->getRandomGenerator()->string();
+    // Create a role 'foo' and add two permissions to it.
+    $role = Role::create([
+      'id' => 'foo',
+      'label' => 'Foo',
+      'is_admin' => FALSE,
+    ]);
+    $role->grantPermission('view own simple_oauth entities');
+    $role->save();
+    $role = Role::create([
+      'id' => 'bar',
+      'label' => 'Bar',
+      'is_admin' => FALSE,
+    ]);
+    $role->grantPermission('administer simple_oauth entities');
+    $role->save();
+    $role = Role::create([
+      'id' => 'oof',
+      'label' => 'Oof',
+      'is_admin' => FALSE,
+    ]);
+    $role->grantPermission('delete own simple_oauth entities');
+    $role->save();
+    $this->user->addRole('foo');
+    $this->user->addRole('bar');
+    $this->user->save();
+
+    // Create a Consumer.
+    $this->client = Consumer::create([
+      'owner_id' => 1,
+      'user_id' => $this->user->id(),
+      'label' => $this->getRandomGenerator()->name(),
+      'secret' => $this->clientSecret,
+      'confidential' => TRUE,
+      'roles' => [['target_id' => 'oof']],
+    ]);
+    $this->client->save();
+
+    // Configure the public and private keys.
+    $path = $this->container->get('module_handler')
+      ->getModule('simple_oauth')
+      ->getPath();
+    $temp_dir = sys_get_temp_dir();
+    $public_path = '/' . $path . '/tests/certificates/public.key';
+    $private_path = '/' . $path . '/tests/certificates/private.key';
+    file_put_contents($temp_dir . '/public.key', file_get_contents(DRUPAL_ROOT . $public_path));
+    file_put_contents($temp_dir . '/private.key', file_get_contents(DRUPAL_ROOT . $private_path));
+    chmod($temp_dir . '/public.key', 0660);
+    chmod($temp_dir . '/private.key', 0660);
+    $this->publicKeyPath = $temp_dir . '/public.key';
+    $this->privateKeyPath = $temp_dir . '/private.key';
+    $settings = $this->config('simple_oauth.settings');
+    $settings->set('public_key', $this->publicKeyPath);
+    $settings->set('private_key', $this->privateKeyPath);
+    $settings->save();
+  }
+
+  /**
+   * Test access to own published node with missing role on User entity.
+   */
+  public function testRequestWithRoleRemovedFromUser() {
+    $access_token = $this->getAccessToken(['foo', 'bar']);
+
+    // Get detailed information about the authenticated user.
+    $response = $this->request(
+      'GET',
+      $this->tokenTestUrl,
+      [
+        'query' => ['_format' => 'json'],
+        'headers' => [
+          'Authorization' => 'Bearer ' . $access_token,
+        ],
+      ]
+    );
+    $parsed_response = Json::decode($response->getBody()->getContents());
+    $this->assertEquals($this->user->id(), $parsed_response['id']);
+    $this->assertEquals(['foo', 'bar', 'authenticated', 'oof'], $parsed_response['roles']);
+    $this->assertTrue($parsed_response['permissions']['view own simple_oauth entities']['access']);
+    $this->assertTrue($parsed_response['permissions']['administer simple_oauth entities']['access']);
+
+    $this->user->removeRole('bar');
+    $this->user->save();
+
+    // We have edited the user, but there was a non-expired existing token for
+    // that user. Even though the TokenUser has the roles assigned, the
+    // underlying user doesn't, so access should not be granted.
+    $response = $this->request(
+      'GET',
+      $this->tokenTestUrl,
+      [
+        'query' => ['_format' => 'json'],
+        'headers' => [
+          'Authorization' => 'Bearer ' . $access_token,
+        ],
+      ]
+    );
+    $parsed_response = Json::decode($response->getBody()->getContents());
+    // The token was successfully removed. The negotiated user is the anonymous
+    // user.
+    $this->assertEquals(0, $parsed_response['id']);
+    $this->assertEquals(['anonymous'], $parsed_response['roles']);
+    $this->assertFalse($parsed_response['permissions']['view own simple_oauth entities']['access']);
+    $this->assertFalse($parsed_response['permissions']['administer simple_oauth entities']['access']);
+
+    // Request the access token again. This time the user doesn't have the role
+    // requested at the time of generating the token.
+    $access_token = $this->getAccessToken(['foo', 'bar']);
+    $response = $this->request(
+      'GET',
+      $this->tokenTestUrl,
+      [
+        'query' => ['_format' => 'json'],
+        'headers' => [
+          'Authorization' => 'Bearer ' . $access_token,
+        ],
+      ]
+    );
+    $parsed_response = Json::decode($response->getBody()->getContents());
+    // The negotiated user is the expected user.
+    $this->assertEquals($this->user->id(), $parsed_response['id']);
+    $this->assertEquals(['foo', 'authenticated', 'oof'], $parsed_response['roles']);
+    $this->assertTrue($parsed_response['permissions']['view own simple_oauth entities']['access']);
+    $this->assertFalse($parsed_response['permissions']['administer simple_oauth entities']['access']);
+  }
+
+  /**
+   * Test access to own unpublished node but with the role removed from client.
+   */
+  public function testRequestWithRoleRemovedFromClient() {
+    $access_token = $this->getAccessToken(['oof']);
+
+    // Get detailed information about the authenticated user.
+    $response = $this->request(
+      'GET',
+      $this->tokenTestUrl,
+      [
+        'query' => ['_format' => 'json'],
+        'headers' => [
+          'Authorization' => 'Bearer ' . $access_token,
+        ],
+      ]
+    );
+    $parsed_response = Json::decode($response->getBody()->getContents());
+    $this->assertEquals($this->user->id(), $parsed_response['id']);
+    $this->assertEquals(['authenticated', 'oof'], $parsed_response['roles']);
+    $this->assertTrue($parsed_response['permissions']['delete own simple_oauth entities']['access']);
+
+    $this->client->set('roles', []);
+    // After saving the client entity, the token should be deleted.
+    $this->client->save();
+
+    // User should NOT have access to view own simple_oauth entities,
+    // because the scope is indicated in the token request, but
+    // missing from the client content entity.
+    $response = $this->request(
+      'GET',
+      $this->tokenTestUrl,
+      [
+        'query' => ['_format' => 'json'],
+        'headers' => [
+          'Authorization' => 'Bearer ' . $access_token,
+        ],
+      ]
+    );
+    $parsed_response = Json::decode($response->getBody()->getContents());
+    // The token was successfully removed. The negotiated user is the anonymous
+    // user.
+    $this->assertEquals(0, $parsed_response['id']);
+    $this->assertEquals(['anonymous'], $parsed_response['roles']);
+    $this->assertFalse($parsed_response['permissions']['view own simple_oauth entities']['access']);
+
+    $access_token = $this->getAccessToken(['oof']);
+    // Get detailed information about the authenticated user.
+    $response = $this->request(
+      'GET',
+      $this->tokenTestUrl,
+      [
+        'query' => ['_format' => 'json'],
+        'headers' => [
+          'Authorization' => 'Bearer ' . $access_token,
+        ],
+      ]
+    );
+    $parsed_response = Json::decode($response->getBody()->getContents());
+    $this->assertEquals($this->user->id(), $parsed_response['id']);
+    $this->assertEquals(['authenticated'], $parsed_response['roles']);
+    $this->assertFalse($parsed_response['permissions']['delete own simple_oauth entities']['access']);
+  }
+
+  /**
+   * Test access to own unpublished node but with missing scope.
+   */
+  public function testRequestWithMissingScope() {
+    $access_token = $this->getAccessToken();
+
+    $response = $this->request(
+      'GET',
+      $this->tokenTestUrl,
+      [
+        'query' => ['_format' => 'json'],
+        'headers' => [
+          'Authorization' => 'Bearer ' . $access_token,
+        ],
+      ]
+    );
+    $parsed_response = Json::decode($response->getBody()->getContents());
+    $this->assertEquals($this->user->id(), $parsed_response['id']);
+    //
+    $this->assertEquals(['authenticated', 'oof'], $parsed_response['roles']);
+    $this->assertFalse($parsed_response['permissions']['view own simple_oauth entities']['access']);
+  }
+
+  /**
+   * Return an access token.
+   *
+   * @param array $scopes
+   *   The scopes.
+   *
+   * @return string
+   *   The access token.
+   */
+  private function getAccessToken(array $scopes = []) {
+    $valid_payload = [
+      'grant_type' => 'client_credentials',
+      'client_id' => $this->client->uuid(),
+      'client_secret' => $this->clientSecret,
+    ];
+    if (!empty($scopes)) {
+      $valid_payload['scope'] = implode(' ', $scopes);
+    }
+    $response = $this->request(
+      'POST',
+      $this->url,
+      ['form_params' => $valid_payload]
+    );
+    $parsed_response = Json::decode($response->getBody()->getContents());
+
+    return $parsed_response['access_token'];
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/AccessTokenAccessControlHandler.php b/drupal/modules/contrib/simple_oauth/src/AccessTokenAccessControlHandler.php
new file mode 100644
index 0000000..64dbb7a
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/AccessTokenAccessControlHandler.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace Drupal\simple_oauth;
+
+use Drupal\consumers\AccessControlHandler;
+
+/**
+ * Access controller for the Access Token entity.
+ *
+ * @see \Drupal\simple_oauth\Entity\AccessToken.
+ */
+class AccessTokenAccessControlHandler extends AccessControlHandler {
+
+  public static $name = 'simple_oauth';
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Annotation/Oauth2Grant.php b/drupal/modules/contrib/simple_oauth/src/Annotation/Oauth2Grant.php
new file mode 100644
index 0000000..2846f3b
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Annotation/Oauth2Grant.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Drupal\simple_oauth\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+
+/**
+ * Defines a OAuth2 Grant item annotation object.
+ *
+ * @see \Drupal\simple_oauth\Plugin\Oauth2GrantManager
+ * @see plugin_api
+ *
+ * @Annotation
+ */
+class Oauth2Grant extends Plugin {
+
+  /**
+   * The plugin ID.
+   *
+   * @var string
+   */
+  public $id;
+
+  /**
+   * The label of the plugin.
+   *
+   * @var \Drupal\Core\Annotation\Translation
+   *
+   * @ingroup plugin_translatable
+   */
+  public $label;
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Authentication/Provider/SimpleOauthAuthenticationProvider.php b/drupal/modules/contrib/simple_oauth/src/Authentication/Provider/SimpleOauthAuthenticationProvider.php
new file mode 100644
index 0000000..c87e928
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Authentication/Provider/SimpleOauthAuthenticationProvider.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Drupal\simple_oauth\Authentication\Provider;
+
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\simple_oauth\Authentication\TokenAuthUser;
+use Drupal\simple_oauth\Server\ResourceServerInterface;
+use League\OAuth2\Server\Exception\OAuthServerException;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * @internal
+ */
+class SimpleOauthAuthenticationProvider implements SimpleOauthAuthenticationProviderInterface {
+
+  /**
+   * @var \Drupal\simple_oauth\Server\ResourceServerInterface
+   */
+  protected $resourceServer;
+
+  /**
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructs a HTTP basic authentication provider object.
+   *
+   * @param \Drupal\simple_oauth\Server\ResourceServerInterface $resource_server
+   *   The resource server object.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager service.
+   */
+  public function __construct(ResourceServerInterface $resource_server, EntityTypeManagerInterface $entity_type_manager) {
+    $this->resourceServer = $resource_server;
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applies(Request $request) {
+    // Check for the presence of the token.
+    return $this->hasTokenValue($request);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function hasTokenValue(Request $request) {
+    // Check the header. See: http://tools.ietf.org/html/rfc6750#section-2.1
+    $auth_header = trim($request->headers->get('Authorization', '', TRUE));
+
+    return strpos($auth_header, 'Bearer ') !== FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function authenticate(Request $request) {
+    // Update the request with the OAuth information.
+    try {
+      $request = $this->resourceServer->validateAuthenticatedRequest($request);
+    }
+    catch (OAuthServerException $exception) {
+      // Procedural code here is hard to avoid.
+      watchdog_exception('simple_oauth', $exception);
+
+      return NULL;
+    }
+
+    $tokens = $this->entityTypeManager->getStorage('oauth2_token')->loadByProperties([
+      'value' => $request->get('oauth_access_token_id'),
+    ]);
+    $token = reset($tokens);
+    return new TokenAuthUser($token);
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Authentication/Provider/SimpleOauthAuthenticationProviderInterface.php b/drupal/modules/contrib/simple_oauth/src/Authentication/Provider/SimpleOauthAuthenticationProviderInterface.php
new file mode 100644
index 0000000..c7416cb
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Authentication/Provider/SimpleOauthAuthenticationProviderInterface.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Drupal\simple_oauth\Authentication\Provider;
+
+use Drupal\Core\Authentication\AuthenticationProviderInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * @internal
+ */
+interface SimpleOauthAuthenticationProviderInterface extends AuthenticationProviderInterface {
+
+
+  /**
+   * Gets the access token from the request.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object.
+   *
+   * @return bool
+   *   TRUE if there is an access token. FALSE otherwise.
+   */
+  public static function hasTokenValue(Request $request);
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Authentication/TokenAuthUser.php b/drupal/modules/contrib/simple_oauth/src/Authentication/TokenAuthUser.php
new file mode 100644
index 0000000..f8d20d1
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Authentication/TokenAuthUser.php
@@ -0,0 +1,865 @@
+<?php
+
+namespace Drupal\simple_oauth\Authentication;
+
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\simple_oauth\Entity\Oauth2TokenInterface;
+use Drupal\user\Entity\User;
+use Drupal\user\UserInterface;
+use League\OAuth2\Server\Exception\OAuthServerException;
+
+/**
+ * @internal
+ */
+class TokenAuthUser implements TokenAuthUserInterface {
+
+  /**
+   * The decorator subject.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $subject;
+
+  /**
+   * The bearer token.
+   *
+   * @var \Drupal\simple_oauth\Entity\Oauth2TokenInterface
+   */
+  protected $token;
+
+  /**
+   * Constructs a TokenAuthUser object.
+   *
+   * @param \Drupal\simple_oauth\Entity\Oauth2TokenInterface $token
+   *   The underlying token.
+   * @throws \Exception
+   *   When there is no user.
+   */
+  public function __construct(Oauth2TokenInterface $token) {
+    if (!$this->subject = $token->get('auth_user_id')->entity) {
+      /** @var \Drupal\consumers\Entity\Consumer $client */
+      if ($client = $token->get('client')->entity) {
+        $this->subject = $client->get('user_id')->entity;
+      }
+    }
+    if (!$this->subject) {
+      throw OAuthServerException::invalidClient();
+    }
+    $this->token = $token;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getToken() {
+    return $this->token;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRoles($exclude_locked_roles = FALSE) {
+    return array_map(function ($item) {
+      return $item['target_id'];
+    }, $this->token->get('scopes')->getValue());
+  }
+
+  /* ---------------------------------------------------------------------------
+     All the methods below are delegated to the decorated user.
+  --------------------------------------------------------------------------- */
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) {
+    return $this->subject->access($operation, $account, $return_as_object);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasPermission($permission) {
+    // User #1 has all privileges.
+    if ((int) $this->id() === 1) {
+      return TRUE;
+    }
+
+    return $this->getRoleStorage()->isPermissionInRoles($permission, $this->getRoles());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isAuthenticated() {
+    return $this->subject->isAuthenticated();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isAnonymous() {
+    return $this->subject->isAnonymous();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPreferredLangcode($fallback_to_default = TRUE) {
+    return $this->subject->getPreferredLangcode($fallback_to_default);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPreferredAdminLangcode($fallback_to_default = TRUE) {
+    return $this->subject->getPreferredAdminLangcode($fallback_to_default);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getUsername() {
+    return $this->subject->getUsername();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getAccountName() {
+    return $this->subject->getAccountName();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDisplayName() {
+    return $this->subject->getDisplayName();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEmail() {
+    return $this->subject->getEmail();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTimeZone() {
+    return $this->subject->getTimeZone();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLastAccessedTime() {
+    return $this->subject->getLastAccessedTime();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    return $this->subject->getCacheContexts();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheTags() {
+    return $this->subject->getCacheTags();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheMaxAge() {
+    return $this->subject->getCacheMaxAge();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasTranslationChanges() {
+    return $this->subject->hasTranslationChanges();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setRevisionTranslationAffected($affected) {
+    return $this->subject->setRevisionTranslationAffected($affected);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isRevisionTranslationAffected() {
+    return $this->subject->isRevisionTranslationAffected();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getChangedTime() {
+    return $this->subject->getChangedTime();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setChangedTime($timestamp) {
+    return $this->subject->setChangedTime($timestamp);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getChangedTimeAcrossTranslations() {
+    return $this->subject->getChangedTimeAcrossTranslations();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function uuid() {
+    return $this->subject->uuid();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function id() {
+    return $this->subject->id();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function language() {
+    return $this->subject->language();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isNew() {
+    return $this->subject->isNew();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function enforceIsNew($value = TRUE) {
+    return $this->subject->enforceIsNew($value);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEntityTypeId() {
+    return $this->subject->getEntityTypeId();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function bundle() {
+    return $this->subject->bundle();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function label() {
+    return $this->subject->label();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function urlInfo($rel = 'canonical', array $options = array()) {
+    return $this->subject->urlInfo($rel, $options);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function url($rel = 'canonical', $options = array()) {
+    return $this->subject->url($rel, $options);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function link($text = NULL, $rel = 'canonical', array $options = []) {
+    return $this->subject->link($text, $rel, $options);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasLinkTemplate($key) {
+    return $this->subject->hasLinkTemplate($key);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function uriRelationships() {
+    return $this->subject->uriRelationships();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function load($id) {
+    return User::load($id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function loadMultiple(array $ids = NULL) {
+    return User::loadMultiple($ids);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(array $values = array()) {
+    return User::create($values);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save() {
+    return $this->subject->save();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function delete() {
+    $this->subject->delete();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preSave(EntityStorageInterface $storage) {
+    $this->subject->preSave($storage);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postSave(EntityStorageInterface $storage, $update = TRUE) {
+    $this->subject->postSave($storage, $update);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function preCreate(EntityStorageInterface $storage, array &$values) {
+    User::preCreate($storage, $values);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postCreate(EntityStorageInterface $storage) {
+    return $this->subject->postCreate($storage);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function preDelete(EntityStorageInterface $storage, array $entities) {
+    User::preDelete($storage, $entities);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function postDelete(EntityStorageInterface $storage, array $entities) {
+    User::postDelete($storage, $entities);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function postLoad(EntityStorageInterface $storage, array &$entities) {
+    User::postLoad($storage, $entities);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createDuplicate() {
+    return $this->subject->createDuplicate();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEntityType() {
+    return $this->subject->getEntityType();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function referencedEntities() {
+    return $this->subject->referencedEntities();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOriginalId() {
+    return $this->subject->getOriginalId();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheTagsToInvalidate() {
+    return $this->subject->getCacheTagsToInvalidate();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setOriginalId($id) {
+    return $this->subject->setOriginalId($id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTypedData() {
+    return $this->subject->getTypedData();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfigDependencyKey() {
+    return $this->subject->getConfigDependencyKey();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfigDependencyName() {
+    return $this->subject->getConfigDependencyName();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfigTarget() {
+    return $this->subject->getConfigTarget();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+    return User::baseFieldDefinitions($entity_type);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function bundleFieldDefinitions(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) {
+    return User::bundleFieldDefinitions($entity_type, $bundle, $base_field_definitions);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasField($field_name) {
+    return $this->subject->hasField($field_name);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFieldDefinition($name) {
+    return $this->subject->getFieldDefinition($name);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFieldDefinitions() {
+    return $this->subject->getFieldDefinitions();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function toArray() {
+    return $this->subject->toArray();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function get($field_name) {
+    return $this->subject->get($field_name);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function set($field_name, $value, $notify = TRUE) {
+    return $this->subject->set($field_name, $value, $notify);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFields($include_computed = TRUE) {
+    return $this->subject->getFields($include_computed);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTranslatableFields($include_computed = TRUE) {
+    return $this->subject->getTranslatableFields($include_computed);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onChange($field_name) {
+    $this->subject->onChange($field_name);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate() {
+    return $this->subject->validate();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isValidationRequired() {
+    return $this->subject->isValidationRequired();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setValidationRequired($required) {
+    return $this->subject->setValidationRequired($required);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addCacheContexts(array $cache_contexts) {
+    return $this->subject->addCacheContexts($cache_contexts);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addCacheTags(array $cache_tags) {
+    return $this->subject->addCacheTags($cache_tags);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function mergeCacheMaxAge($max_age) {
+    return $this->subject->mergeCacheMaxAge($max_age);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addCacheableDependency($other_object) {
+    return $this->subject->addCacheableDependency($other_object);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isNewRevision() {
+    return $this->subject->isNewRevision();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setNewRevision($value = TRUE) {
+    $this->subject->setNewRevision($value);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRevisionId() {
+    return $this->subject->getRevisionId();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isDefaultRevision($new_value = NULL) {
+    return $this->subject->isDefaultRevision($new_value);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preSaveRevision(EntityStorageInterface $storage, \stdClass $record) {
+    $this->subject->preSaveRevision($storage, $record);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isDefaultTranslation() {
+    return $this->subject->isDefaultTranslation();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTranslationLanguages($include_default = TRUE) {
+    return $this->subject->getTranslationLanguages($include_default);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTranslation($langcode) {
+    return $this->subject->getTranslation($langcode);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getUntranslated() {
+    return $this->subject->getUntranslated();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasTranslation($langcode) {
+    return $this->subject->hasTranslation($langcode);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addTranslation($langcode, array $values = array()) {
+    return $this->subject->addTranslation($langcode, $values);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function removeTranslation($langcode) {
+    $this->subject->removeTranslation($langcode);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isTranslatable() {
+    return $this->subject->isTranslatable();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasRole($rid) {
+    return in_array($rid, $this->getRoles());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addRole($rid) {
+    $this->subject->addRole($rid);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function removeRole($rid) {
+    $this->subject->removeRole($rid);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUsername($username) {
+    return $this->subject->setUsername($username);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPassword() {
+    return $this->subject->getPassword();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setPassword($password) {
+    return $this->subject->setPassword($password);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setEmail($mail) {
+    return $this->subject->setEmail($mail);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCreatedTime() {
+    return $this->subject->getCreatedTime();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setLastAccessTime($timestamp) {
+    return $this->subject->setLastAccessTime($timestamp);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLastLoginTime() {
+    return $this->subject->getLastLoginTime();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setLastLoginTime($timestamp) {
+    return $this->subject->setLastLoginTime($timestamp);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isActive() {
+    return $this->subject->isActive();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isBlocked() {
+    return $this->subject->isBlocked();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function activate() {
+    return $this->subject->activate();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function block() {
+    return $this->subject->block();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInitialEmail() {
+    return $this->subject->getInitialEmail();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setExistingPassword($password) {
+    return $this->subject->setExistingPassword($password);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function checkExistingPassword(UserInterface $account_unchanged) {
+    return $this->subject->checkExistingPassword($account_unchanged);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getIterator() {
+    throw new \Exception('Invalid use of getIterator in token authentication.');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function toUrl($rel = 'canonical', array $options = array()) {
+    $this->subject->toUrl($rel, $options);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function toLink($text = NULL, $rel = 'canonical', array $options = []) {
+    return $this->subject->toLink($text, $rel, $options);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isNewTranslation() {
+    return $this->subject->isNewTranslation();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLoadedRevisionId() {
+    return $this->subject->getLoadedRevisionId();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function updateLoadedRevisionId() {
+    return $this->subject->updateLoadedRevisionId();
+  }
+
+  /**
+   * Returns the role storage object.
+   *
+   * @return \Drupal\user\RoleStorageInterface
+   *   The role storage object.
+   */
+  protected function getRoleStorage() {
+    return \Drupal::entityTypeManager()->getStorage('user_role');
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Authentication/TokenAuthUserInterface.php b/drupal/modules/contrib/simple_oauth/src/Authentication/TokenAuthUserInterface.php
new file mode 100644
index 0000000..539514d
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Authentication/TokenAuthUserInterface.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Drupal\simple_oauth\Authentication;
+
+use Drupal\user\UserInterface;
+
+interface TokenAuthUserInterface extends \IteratorAggregate, UserInterface {
+
+  /**
+   * Get the token.
+   *
+   * @return \Drupal\simple_oauth\Entity\Oauth2TokenInterface
+   *   The provided OAuth2 token.
+   */
+  public function getToken();
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Controller/Oauth2Token.php b/drupal/modules/contrib/simple_oauth/src/Controller/Oauth2Token.php
new file mode 100644
index 0000000..33f6a19
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Controller/Oauth2Token.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace Drupal\simple_oauth\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\simple_oauth\Plugin\Oauth2GrantManagerInterface;
+use GuzzleHttp\Psr7\Response;
+use League\OAuth2\Server\AuthorizationServer;
+use League\OAuth2\Server\Exception\OAuthServerException;
+use Psr\Http\Message\ServerRequestInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+class Oauth2Token extends ControllerBase {
+
+  /**
+   * @var \Drupal\simple_oauth\Plugin\Oauth2GrantManagerInterface
+   */
+  protected $grantManager;
+
+  /**
+   * Oauth2Token constructor.
+   *
+   * @param \Drupal\simple_oauth\Plugin\Oauth2GrantManagerInterface $grant_manager
+   */
+  public function __construct(Oauth2GrantManagerInterface $grant_manager) {
+    $this->grantManager = $grant_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('plugin.manager.oauth2_grant.processor')
+    );
+  }
+
+  /**
+   * Processes POST requests to /oauth/token.
+   */
+  public function token(ServerRequestInterface $request) {
+    // Extract the grant type from the request body.
+    $body = $request->getParsedBody();
+    $grant_type_id = !empty($body['grant_type']) ? $body['grant_type'] : 'implicit';
+    // Get the auth server object from that uses the League library.
+    try {
+      // Respond to the incoming request and fill in the response.
+      $auth_server = $this->grantManager->getAuthorizationServer($grant_type_id);
+      $response = $this->handleToken($request, $auth_server);
+    }
+    catch (OAuthServerException $exception) {
+      watchdog_exception('simple_oauth', $exception);
+      $response = $exception->generateHttpResponse(new Response());
+    }
+    return $response;
+  }
+
+  /**
+   * Handles the token processing.
+   *
+   * @param \Psr\Http\Message\ServerRequestInterface $psr7_request
+   *
+   * @return \Psr\Http\Message\ResponseInterface
+   */
+  protected function handleToken(ServerRequestInterface $psr7_request, AuthorizationServer $auth_server) {
+    // Instantiate a new PSR-7 response object so the library can fill it.
+    return $auth_server->respondToAccessTokenRequest($psr7_request, new Response());
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Entities/AccessTokenEntity.php b/drupal/modules/contrib/simple_oauth/src/Entities/AccessTokenEntity.php
new file mode 100644
index 0000000..11c2859
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Entities/AccessTokenEntity.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Drupal\simple_oauth\Entities;
+
+use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
+use League\OAuth2\Server\Entities\Traits\AccessTokenTrait;
+use League\OAuth2\Server\Entities\Traits\EntityTrait;
+use League\OAuth2\Server\Entities\Traits\TokenEntityTrait;
+
+class AccessTokenEntity implements AccessTokenEntityInterface {
+
+  use AccessTokenTrait, TokenEntityTrait, EntityTrait;
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Entities/ClientEntity.php b/drupal/modules/contrib/simple_oauth/src/Entities/ClientEntity.php
new file mode 100644
index 0000000..19dfb7c
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Entities/ClientEntity.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Drupal\simple_oauth\Entities;
+
+use Drupal\consumers\Entity\Consumer;
+use League\OAuth2\Server\Entities\Traits\ClientTrait;
+use League\OAuth2\Server\Entities\Traits\EntityTrait;
+
+class ClientEntity implements ClientEntityInterface {
+
+  use EntityTrait, ClientTrait;
+
+  /**
+   * @var \Drupal\consumers\Entity\Consumer
+   */
+  protected $entity;
+
+  /**
+   * ClientEntity constructor.
+   *
+   * @param \Drupal\consumers\Entity\Consumer $entity
+   *   The Drupal entity.
+   */
+  public function __construct(Consumer $entity) {
+    $this->entity = $entity;
+    $this->setIdentifier($entity->uuid());
+    $this->setName($entity->label());
+    if ($entity->hasField('redirect')) {
+      $this->redirectUri = $entity->get('redirect')->value;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setName($name) {
+    $this->name = $name;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDrupalEntity() {
+    return $this->entity;
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Entities/ClientEntityInterface.php b/drupal/modules/contrib/simple_oauth/src/Entities/ClientEntityInterface.php
new file mode 100644
index 0000000..7e8d49e
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Entities/ClientEntityInterface.php
@@ -0,0 +1,26 @@
+<?php
+
+
+namespace Drupal\simple_oauth\Entities;
+
+use \League\OAuth2\Server\Entities\ClientEntityInterface as LeagueClientEntityInterface;
+
+interface ClientEntityInterface extends LeagueClientEntityInterface {
+
+  /**
+   * Set the name of the client.
+   *
+   * @param string $name
+   *   The name to set.
+   */
+  public function setName($name);
+
+  /**
+   * Returns the associated Drupal entity.
+   *
+   * @return \Drupal\consumers\Entity\Consumer
+   *   The Drupal entity.
+   */
+  public function getDrupalEntity();
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Entities/RefreshTokenEntity.php b/drupal/modules/contrib/simple_oauth/src/Entities/RefreshTokenEntity.php
new file mode 100644
index 0000000..528e70d
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Entities/RefreshTokenEntity.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace Drupal\simple_oauth\Entities;
+
+use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
+use League\OAuth2\Server\Entities\Traits\EntityTrait;
+use League\OAuth2\Server\Entities\Traits\RefreshTokenTrait;
+
+class RefreshTokenEntity implements RefreshTokenEntityInterface {
+
+  use RefreshTokenTrait, EntityTrait;
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Entities/ScopeEntity.php b/drupal/modules/contrib/simple_oauth/src/Entities/ScopeEntity.php
new file mode 100644
index 0000000..03e9254
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Entities/ScopeEntity.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Drupal\simple_oauth\Entities;
+
+use Drupal\user\RoleInterface;
+use League\OAuth2\Server\Entities\ScopeEntityInterface;
+use League\OAuth2\Server\Entities\Traits\EntityTrait;
+
+class ScopeEntity implements ScopeEntityInterface {
+
+  use EntityTrait;
+
+  /**
+   * Construct a ScopeEntity instance.
+   *
+   * @param \Drupal\User\RoleInterface $role
+   *   The role associated to the scope.
+   */
+  public function __construct(RoleInterface $role) {
+    $this->role = $role;
+    $this->setIdentifier($role->id());
+  }
+
+
+  public function jsonSerialize() {
+    return $this->getIdentifier();
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Entities/UserEntity.php b/drupal/modules/contrib/simple_oauth/src/Entities/UserEntity.php
new file mode 100644
index 0000000..1f8ef19
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Entities/UserEntity.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace Drupal\simple_oauth\Entities;
+
+use League\OAuth2\Server\Entities\Traits\EntityTrait;
+use League\OAuth2\Server\Entities\UserEntityInterface;
+
+class UserEntity implements UserEntityInterface {
+
+  use EntityTrait;
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Entity/ConfigEntityLockableInterface.php b/drupal/modules/contrib/simple_oauth/src/Entity/ConfigEntityLockableInterface.php
new file mode 100644
index 0000000..cab82bb
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Entity/ConfigEntityLockableInterface.php
@@ -0,0 +1,26 @@
+<?php
+
+
+namespace Drupal\simple_oauth\Entity;
+
+
+interface ConfigEntityLockableInterface {
+
+  /**
+   * Checks if the entity is locked against changes.
+   *
+   * @return bool
+   */
+  public function isLocked();
+
+  /**
+   * Locks the entity.
+   */
+  public function lock();
+
+  /**
+   * Unlocks the entity.
+   */
+  public function unlock();
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Entity/ConfigEntityLockableTrait.php b/drupal/modules/contrib/simple_oauth/src/Entity/ConfigEntityLockableTrait.php
new file mode 100644
index 0000000..8c16bcb
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Entity/ConfigEntityLockableTrait.php
@@ -0,0 +1,37 @@
+<?php
+
+
+namespace Drupal\simple_oauth\Entity;
+
+trait ConfigEntityLockableTrait {
+
+  /**
+   * Locked status.
+   *
+   * @var bool
+   */
+  protected $locked = FALSE;
+
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isLocked() {
+    return $this->locked;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function lock() {
+    $this->locked = TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function unlock() {
+    $this->locked = FALSE;
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Entity/Form/Oauth2TokenDeleteForm.php b/drupal/modules/contrib/simple_oauth/src/Entity/Form/Oauth2TokenDeleteForm.php
new file mode 100644
index 0000000..04fe84b
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Entity/Form/Oauth2TokenDeleteForm.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace Drupal\simple_oauth\Entity\Form;
+
+use Drupal\Core\Entity\ContentEntityConfirmFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+
+/**
+ * Provides a form for deleting Access Token entities.
+ *
+ * @ingroup simple_oauth
+ */
+class Oauth2TokenDeleteForm extends ContentEntityConfirmFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    return $this->t('Are you sure you want to delete entity %name?', array('%name' => $this->entity->label()));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelUrl() {
+    return $this->getEntity()->toUrl('canonical');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmText() {
+    return $this->t('Delete');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $this->entity->delete();
+
+    drupal_set_message(
+      $this->t('content @type: deleted @label.',
+        [
+          '@type' => $this->entity->bundle(),
+          '@label' => $this->entity->label()
+        ]
+        )
+    );
+
+    $form_state->setRedirectUrl(new Url('entity.oauth2_token.collection'));
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Entity/Form/Oauth2TokenSettingsForm.php b/drupal/modules/contrib/simple_oauth/src/Entity/Form/Oauth2TokenSettingsForm.php
new file mode 100644
index 0000000..1351cb4
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Entity/Form/Oauth2TokenSettingsForm.php
@@ -0,0 +1,115 @@
+<?php
+
+namespace Drupal\simple_oauth\Entity\Form;
+
+use Drupal\Core\Form\ConfigFormBase;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * @internal
+ */
+class Oauth2TokenSettingsForm extends ConfigFormBase {
+
+  /**
+   * Returns a unique string identifying the form.
+   *
+   * @return string
+   *   The unique string identifying the form.
+   */
+  public function getFormId() {
+    return 'oauth2_token_settings';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEditableConfigNames() {
+    return ['simple_oauth.settings'];
+  }
+
+  /**
+   * Form submission handler.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $settings = $this->config('simple_oauth.settings');
+    $settings->set('access_token_expiration', $form_state->getValue('access_token_expiration'));
+    $settings->set('refresh_token_expiration', $form_state->getValue('refresh_token_expiration'));
+    $settings->set('public_key', $form_state->getValue('public_key'));
+    $settings->set('private_key', $form_state->getValue('private_key'));
+    $settings->save();
+    parent::submitForm($form, $form_state);
+  }
+
+  /**
+   * Defines the settings form for Access Token entities.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return array
+   *   Form definition array.
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form['access_token_expiration'] = [
+      '#type' => 'number',
+      '#title' => $this->t('Access token expiration time'),
+      '#description' => $this->t('The default value, in seconds, to be used as expiration time when creating new tokens.'),
+      '#default_value' => $this->config('simple_oauth.settings')->get('access_token_expiration'),
+    ];
+    $form['refresh_token_expiration'] = [
+      '#type' => 'number',
+      '#title' => $this->t('Refresh token expiration time'),
+      '#description' => $this->t('The default value, in seconds, to be used as expiration time when creating new tokens.'),
+      '#default_value' => $this->config('simple_oauth.settings')->get('refresh_token_expiration'),
+    ];
+    $form['public_key'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Public Key'),
+      '#description' => $this->t('The path to the public key file.'),
+      '#default_value' => $this->config('simple_oauth.settings')->get('public_key'),
+      '#element_validate' => ['::validateExistingFile'],
+      '#required' => TRUE,
+    ];
+    $form['private_key'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Private Key'),
+      '#description' => $this->t('The path to the private key file.'),
+      '#default_value' => $this->config('simple_oauth.settings')->get('private_key'),
+      '#element_validate' => ['::validateExistingFile'],
+      '#required' => TRUE,
+    ];
+    return parent::buildForm($form, $form_state);
+  }
+
+  /**
+   * Validates if the file exists.
+   *
+   * @param array $element
+   *   The element being processed.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   * @param array $complete_form
+   *   The complete form structure.
+   */
+  public function validateExistingFile(&$element, FormStateInterface $form_state, &$complete_form) {
+    if (!empty($element['#value'])) {
+      $path = $element['#value'];
+      // Does the file exist?
+      if (!file_exists($path)) {
+        $form_state->setError($element, $this->t('The %field file does not exist.', ['%field' => $element['#title']]));
+      }
+      // Is the file readable?
+      if (!is_readable($path)) {
+        $form_state->setError($element, $this->t('The %field file at the specified location is not readable.', ['%field' => $element['#title']]));
+      }
+    }
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Entity/Oauth2Token.php b/drupal/modules/contrib/simple_oauth/src/Entity/Oauth2Token.php
new file mode 100644
index 0000000..846f15e
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Entity/Oauth2Token.php
@@ -0,0 +1,224 @@
+<?php
+
+namespace Drupal\simple_oauth\Entity;
+
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Entity\ContentEntityBase;
+use Drupal\Core\Entity\EntityChangedTrait;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\user\UserInterface;
+
+/**
+ * Defines the Oauth2 Token entity.
+ *
+ * @ingroup simple_oauth
+ *
+ * @ContentEntityType(
+ *   id = "oauth2_token",
+ *   label = @Translation("OAuth2 token"),
+ *   bundle_label = @Translation("Token type"),
+ *   handlers = {
+ *     "list_builder" = "Drupal\simple_oauth\Oauth2TokenListBuilder",
+ *     "form" = {
+ *       "delete" = "Drupal\simple_oauth\Entity\Form\Oauth2TokenDeleteForm",
+ *     },
+ *     "access" = "Drupal\simple_oauth\AccessTokenAccessControlHandler",
+ *   },
+ *   base_table = "oauth2_token",
+ *   admin_permission = "administer simple_oauth entities",
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "label" = "value",
+ *     "bundle" = "bundle",
+ *     "uuid" = "uuid"
+ *   },
+ *   bundle_entity_type = "oauth2_token_type",
+ *   links = {
+ *     "canonical" = "/admin/content/simple_oauth/{oauth2_token}",
+ *     "delete-form" = "/admin/content/simple_oauth/{oauth2_token}/delete"
+ *   }
+ * )
+ */
+class Oauth2Token extends ContentEntityBase implements Oauth2TokenInterface {
+
+  use EntityChangedTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+    $fields = parent::baseFieldDefinitions($entity_type);
+
+    $fields['id'] = BaseFieldDefinition::create('integer')
+      ->setLabel(t('ID'))
+      ->setDescription(t('The ID of the Access Token entity.'))
+      ->setReadOnly(TRUE);
+
+    $fields['uuid'] = BaseFieldDefinition::create('uuid')
+      ->setLabel(t('UUID'))
+      ->setDescription(t('The UUID of the Access Token entity.'))
+      ->setReadOnly(TRUE);
+
+    $fields['bundle'] = BaseFieldDefinition::create('entity_reference')
+      ->setLabel(t('Bundle'))
+      ->setDescription(t('The bundle property.'))
+      ->setRevisionable(FALSE)
+      ->setReadOnly(TRUE)
+      ->setDisplayOptions('view', array(
+        'label' => 'inline',
+        'type' => 'entity_reference_label',
+        'weight' => 0,
+      ))
+      ->setSetting('target_type', 'oauth2_token_type');
+
+    $fields['auth_user_id'] = BaseFieldDefinition::create('entity_reference')
+      ->setLabel(t('User'))
+      ->setDescription(t('The user ID of the user this access token is authenticating.'))
+      ->setRevisionable(TRUE)
+      ->setSetting('target_type', 'user')
+      ->setSetting('handler', 'default')
+      ->setDefaultValueCallback('Drupal\node\Entity\Node::getCurrentUserId')
+      ->setTranslatable(FALSE)
+      ->setDisplayOptions('view', array(
+        'label' => 'inline',
+        'type' => 'author',
+        'weight' => 1,
+      ))
+      ->setCardinality(1)
+      ->setDisplayOptions('form', array(
+        'type' => 'entity_reference_autocomplete',
+        'weight' => 0,
+        'settings' => array(
+          'match_operator' => 'CONTAINS',
+          'size' => '60',
+          'autocomplete_type' => 'tags',
+          'placeholder' => '',
+        ),
+      ));
+
+    $fields['client'] = BaseFieldDefinition::create('entity_reference')
+      ->setLabel(t('Client'))
+      ->setDescription(t('The consumer client for this Access Token.'))
+      ->setRevisionable(TRUE)
+      ->setSetting('target_type', 'consumer')
+      ->setSetting('handler', 'default')
+      ->setTranslatable(FALSE)
+      ->setDisplayOptions('view', array(
+        'label' => 'inline',
+        'type' => 'entity_reference_label',
+        'weight' => 2,
+      ))
+      ->setDisplayOptions('form', array(
+        'type' => 'entity_reference_autocomplete',
+        'weight' => 2,
+      ));
+
+    $fields['scopes'] = BaseFieldDefinition::create('entity_reference')
+      ->setLabel(t('Scopes'))
+      ->setDescription(t('The scopes for this Access Token. OAuth2 scopes are implemented as Drupal roles.'))
+      ->setRevisionable(TRUE)
+      ->setSetting('target_type', 'user_role')
+      ->setSetting('handler', 'default')
+      ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED)
+      ->setTranslatable(FALSE)
+      ->setDisplayOptions('view', array(
+        'label' => 'inline',
+        'type' => 'entity_reference_label',
+        'weight' => 3,
+      ))
+      ->setDisplayOptions('form', array(
+        'type' => 'entity_reference_autocomplete',
+        'weight' => 3,
+        'settings' => array(
+          'match_operator' => 'CONTAINS',
+          'size' => '60',
+          'autocomplete_type' => 'tags',
+          'placeholder' => '',
+        ),
+      ));
+
+    $fields['value'] = BaseFieldDefinition::create('string')
+      ->setLabel(t('Token'))
+      ->setDescription(t('The token value.'))
+      ->setSettings(array(
+        'max_length' => 128,
+        'text_processing' => 0,
+      ))
+      ->setRequired(TRUE)
+      ->setDisplayOptions('view', array(
+        'label' => 'inline',
+        'type' => 'timestamp',
+        'weight' => 4,
+      ));
+
+    $fields['created'] = BaseFieldDefinition::create('created')
+      ->setLabel(t('Created'))
+      ->setDescription(t('The time that the entity was created.'))
+      ->setDisplayOptions('view', array(
+        'label' => 'inline',
+        'type' => 'timestamp',
+        'weight' => 5,
+      ));
+
+    $fields['changed'] = BaseFieldDefinition::create('changed')
+      ->setLabel(t('Changed'))
+      ->setDescription(t('The time that the entity was last edited.'))
+      ->setDisplayOptions('view', array(
+        'label' => 'inline',
+        'type' => 'timestamp',
+        'weight' => 6,
+      ));
+
+    $fields['expire'] = BaseFieldDefinition::create('timestamp')
+      ->setLabel(t('Expire'))
+      ->setDescription(t('The time when the token expires.'))
+      ->setDisplayOptions('form', array(
+        'type' => 'datetime_timestamp',
+        'weight' => 7,
+      ))
+      ->setDisplayOptions('view', array(
+        'label' => 'inline',
+        'type' => 'timestamp',
+        'weight' => 7,
+      ))
+      ->setRequired(TRUE);
+
+    $fields['status'] = BaseFieldDefinition::create('boolean')
+      ->setLabel(t('Publishing status'))
+      ->setDescription(t('A boolean indicating whether the token is available.'))
+      ->setDisplayOptions('view', array(
+        'label' => 'inline',
+        'type' => 'boolean',
+        'weight' => 8,
+      ))
+      ->setRevisionable(TRUE)
+      ->setTranslatable(TRUE)
+      ->setDefaultValue(TRUE);
+
+    return $fields;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCreatedTime() {
+    return $this->get('created')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function revoke() {
+    $this->set('status', FALSE);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isRevoked() {
+    return !$this->get('status')->value;
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Entity/Oauth2TokenInterface.php b/drupal/modules/contrib/simple_oauth/src/Entity/Oauth2TokenInterface.php
new file mode 100644
index 0000000..38436fa
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Entity/Oauth2TokenInterface.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Drupal\simple_oauth\Entity;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityChangedInterface;
+
+/**
+ * Provides an interface defining Access Token entities.
+ *
+ * @ingroup simple_oauth
+ */
+interface Oauth2TokenInterface extends ContentEntityInterface, EntityChangedInterface {
+
+  /**
+   * Revoke a token.
+   */
+  public function revoke();
+
+  /**
+   * Check if the token was revoked.
+   *
+   * @return bool
+   *   TRUE if the token is revoked. FALSE otherwise.
+   */
+  public function isRevoked();
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Entity/Oauth2TokenType.php b/drupal/modules/contrib/simple_oauth/src/Entity/Oauth2TokenType.php
new file mode 100644
index 0000000..54ff06c
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Entity/Oauth2TokenType.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace Drupal\simple_oauth\Entity;
+
+use Drupal\Core\Config\Entity\ConfigEntityBase;
+
+/**
+ * Defines the OAuth2 Token Type entity.
+ *
+ * @ConfigEntityType(
+ *   id = "oauth2_token_type",
+ *   label = @Translation("OAuth2 Token Type"),
+ *   handlers = {
+ *     "access" = "Drupal\simple_oauth\LockableConfigEntityAccessControlHandler"
+ *   },
+ *   config_prefix = "oauth2_token.bundle",
+ *   admin_permission = "administer simple_oauth entities",
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "label" = "label",
+ *     "uuid" = "uuid"
+ *   },
+ *   links = {
+ *     "canonical" = "/admin/config/people/accounts/oauth2_token_type/{oauth2_token_type}",
+ *   }
+ * )
+ */
+class Oauth2TokenType extends ConfigEntityBase implements Oauth2TokenTypeInterface {
+
+  use ConfigEntityLockableTrait;
+
+  /**
+   * The Access Token Type ID.
+   *
+   * @var string
+   */
+  protected $id;
+
+  /**
+   * The Access Token Type label.
+   *
+   * @var string
+   */
+  protected $label;
+
+  /**
+   * The Access Token Type label.
+   *
+   * @var string
+   */
+  protected $description = '';
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    return $this->description;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setDescription($description) {
+    $this->description = $description;
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Entity/Oauth2TokenTypeInterface.php b/drupal/modules/contrib/simple_oauth/src/Entity/Oauth2TokenTypeInterface.php
new file mode 100644
index 0000000..992a177
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Entity/Oauth2TokenTypeInterface.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Drupal\simple_oauth\Entity;
+
+/**
+ * Provides an interface defining Access Token Type entities.
+ */
+interface Oauth2TokenTypeInterface extends ConfigEntityLockableInterface  {
+
+  /**
+   * Get the description.
+   *
+   * @return string
+   *   The description
+   */
+  public function getDescription();
+
+  /**
+   * Set the description.
+   *
+   * @param string $description
+   *   The description
+   */
+  public function setDescription($description);
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/ExpiredCollector.php b/drupal/modules/contrib/simple_oauth/src/ExpiredCollector.php
new file mode 100644
index 0000000..919c48a
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/ExpiredCollector.php
@@ -0,0 +1,131 @@
+<?php
+
+namespace Drupal\simple_oauth;
+
+use Drupal\Component\Datetime\TimeInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Entity\Query\QueryException;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\consumers\Entity\Consumer;
+
+/**
+ * Service in charge of deleting or expiring tokens that cannot be used anymore.
+ */
+class ExpiredCollector {
+
+  /**
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $tokenStorage;
+
+  /**
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $clientStorage;
+
+  /**
+   * @var \Drupal\Component\Datetime\TimeInterface
+   */
+  protected $dateTime;
+
+  /**
+   * ExpiredCollector constructor.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\Component\Datetime\TimeInterface $date_time
+   *   The date time service.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, TimeInterface $date_time) {
+    $this->clientStorage = $entity_type_manager->getStorage('consumer');
+    $this->tokenStorage = $entity_type_manager->getStorage('oauth2_token');
+    $this->dateTime = $date_time;
+  }
+
+  /**
+   * Collect all expired token ids.
+   *
+   * @return \Drupal\simple_oauth\Entity\Oauth2TokenInterface[]
+   *   The expired tokens.
+   */
+  public function collect() {
+    $query = $this->tokenStorage->getQuery();
+    $query->condition('expire', $this->dateTime->getRequestTime(), '<');
+    if (!$results = $query->execute()) {
+      return [];
+    }
+    return array_values($this->tokenStorage->loadMultiple(array_values($results)));
+  }
+
+  /**
+   * Collect all the tokens associated with the provided account.
+   *
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The account.
+   *
+   * @return \Drupal\simple_oauth\Entity\Oauth2TokenInterface[]
+   *   The tokens.
+   */
+  public function collectForAccount(AccountInterface $account) {
+    $query = $this->tokenStorage->getQuery();
+    $query->condition('auth_user_id', $account->id());
+    $entity_ids = $query->execute();
+    $output = $entity_ids
+      ? array_values($this->tokenStorage->loadMultiple(array_values($entity_ids)))
+      : [];
+    // Also collect the tokens of the clients that have this account as the
+    // default user.
+    try {
+      $clients = array_values($this->clientStorage->loadByProperties([
+        'user_id' => $account->id(),
+      ]));
+    }
+    catch (QueryException $exception) {
+      // This happens when simple_oauth_extras is not enabled because the
+      // 'user_id' field is not available.
+      return $output;
+    }
+    // Append all the tokens for each of the clients having this account as the
+    // default.
+    $tokens = array_reduce($clients, function ($carry, $client) {
+      return array_merge($carry, $this->collectForClient($client));
+    }, $output);
+    // Return a unique list.
+    $existing = [];
+    foreach ($tokens as $token) {
+      $existing[$token->id()] = $token;
+    }
+    return array_values($existing);
+  }
+
+  /**
+   * Collect all the tokens associated a particular client.
+   *
+   * @param \Drupal\consumers\Entity\Consumer $client
+   *   The account.
+   *
+   * @return \Drupal\simple_oauth\Entity\Oauth2TokenInterface[]
+   *   The tokens.
+   */
+  public function collectForClient(Consumer $client) {
+    $query = $this->tokenStorage->getQuery();
+    $query->condition('client', $client->id());
+    if (!$entity_ids = $query->execute()) {
+      return [];
+    }
+    /** @var \Drupal\simple_oauth\Entity\Oauth2TokenInterface[] $results */
+    $results = $this->tokenStorage->loadMultiple(array_values($entity_ids));
+    return array_values($results);
+  }
+
+  /**
+   * Deletes multiple tokens based on ID.
+   *
+   * @param \Drupal\simple_oauth\Entity\Oauth2TokenInterface[] $tokens
+   *   The token entity IDs.
+   */
+  public function deleteMultipleTokens(array $tokens = []) {
+    $this->tokenStorage->delete($tokens);
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/HttpMiddleware/BasicAuthSwap.php b/drupal/modules/contrib/simple_oauth/src/HttpMiddleware/BasicAuthSwap.php
new file mode 100644
index 0000000..03f6660
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/HttpMiddleware/BasicAuthSwap.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace Drupal\simple_oauth\HttpMiddleware;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
+
+/**
+ * Uses the basic auth information to provide the client credentials for OAuth2.
+ */
+class BasicAuthSwap implements HttpKernelInterface {
+
+  /**
+   * The wrapped HTTP kernel.
+   *
+   * @var \Symfony\Component\HttpKernel\HttpKernelInterface
+   */
+  protected $httpKernel;
+
+  /**
+   * Constructs a BasicAuthSwap object.
+   *
+   * @param \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel
+   *   The decorated kernel.
+   */
+  public function __construct(HttpKernelInterface $http_kernel) {
+    $this->httpKernel = $http_kernel;
+  }
+
+  /**
+   * Handles a Request to convert it to a Response.
+   *
+   * If the request appears to be an OAuth2 token request with Basic Auth,
+   * swap the Basic Auth credentials into the request body and then remove the
+   * Basic Auth credentials from the request so that core authentication is
+   * not performed later.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The input request.
+   * @param int $type
+   *   The type of the request. One of HttpKernelInterface::MASTER_REQUEST or
+   *   HttpKernelInterface::SUB_REQUEST.
+   * @param bool $catch
+   *   Whether to catch exceptions or not.
+   *
+   * @throws \Exception
+   *   When an Exception occurs during processing.
+   *
+   * @return \Symfony\Component\HttpFoundation\Response
+   *   A Response instance
+   */
+  public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) {
+    if (
+      strpos($request->getPathInfo(), '/oauth/token') !== FALSE &&
+      $request->headers->has('PHP_AUTH_USER') &&
+      $request->headers->has('PHP_AUTH_PW')
+    ) {
+      // Swap the Basic Auth credentials into the request data.
+      $request->request->set('client_id', $request->headers->get('PHP_AUTH_USER'));
+      $request->request->set('client_secret', $request->headers->get('PHP_AUTH_PW'));
+
+      // Remove the Basic Auth credentials to prevent later authentication.
+      $request->headers->remove('PHP_AUTH_USER');
+      $request->headers->remove('PHP_AUTH_PW');
+    }
+
+    return $this->httpKernel->handle($request, $type, $catch);
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/LockableConfigEntityAccessControlHandler.php b/drupal/modules/contrib/simple_oauth/src/LockableConfigEntityAccessControlHandler.php
new file mode 100644
index 0000000..97bb054
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/LockableConfigEntityAccessControlHandler.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Drupal\simple_oauth;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Entity\EntityAccessControlHandler;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Session\AccountInterface;
+
+class LockableConfigEntityAccessControlHandler extends EntityAccessControlHandler {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
+    if ($operation == 'view') {
+      // Allow viewing the configuration entity.
+      return AccessResult::allowed();
+    }
+    if ($entity->isLocked()) {
+      return AccessResult::forbidden();
+    }
+    return parent::checkAccess($entity, $operation, $account);
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Normalizer/RefreshTokenEntityNormalizer.php b/drupal/modules/contrib/simple_oauth/src/Normalizer/RefreshTokenEntityNormalizer.php
new file mode 100644
index 0000000..285ec0e
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Normalizer/RefreshTokenEntityNormalizer.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Drupal\simple_oauth\Normalizer;
+
+use Drupal\serialization\Normalizer\NormalizerBase;
+use Symfony\Component\Serializer\Normalizer\scalar;
+
+class RefreshTokenEntityNormalizer extends NormalizerBase implements TokenEntityNormalizerInterface {
+
+  /**
+   * The interface or class that this Normalizer supports.
+   *
+   * @var string|array
+   */
+  protected $supportedInterfaceOrClass = '\League\OAuth2\Server\Entities\RefreshTokenEntityInterface';
+
+  /**
+   * {@inheritdoc}
+   */
+  public function normalize($token_entity, $format = NULL, array $context = array()) {
+    /** @var \League\OAuth2\Server\Entities\TokenInterface $token_entity */
+    return [
+      'value' => $token_entity->getIdentifier(),
+      'expire' => $token_entity->getExpiryDateTime()->format('U'),
+    ];
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Normalizer/TokenEntityNormalizer.php b/drupal/modules/contrib/simple_oauth/src/Normalizer/TokenEntityNormalizer.php
new file mode 100644
index 0000000..954fb81
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Normalizer/TokenEntityNormalizer.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Drupal\simple_oauth\Normalizer;
+
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\serialization\Normalizer\NormalizerBase;
+use Symfony\Component\Serializer\Normalizer\scalar;
+
+class TokenEntityNormalizer extends NormalizerBase implements TokenEntityNormalizerInterface {
+
+  /**
+   * The interface or class that this Normalizer supports.
+   *
+   * @var string|array
+   */
+  protected $supportedInterfaceOrClass = '\League\OAuth2\Server\Entities\TokenInterface';
+
+  /**
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+
+  /**
+   * {@inheritdoc}
+   */
+  public function normalize($token_entity, $format = NULL, array $context = array()) {
+    /** @var \League\OAuth2\Server\Entities\TokenInterface $token_entity */
+
+    $scopes = array_map(function ($scope_entity) {
+      /** @var \League\OAuth2\Server\Entities\ScopeEntityInterface $scope_entity */
+      return ['target_id' => $scope_entity->getIdentifier()];
+    }, $token_entity->getScopes());
+
+    /** @var \Drupal\simple_oauth\Entities\ClientEntityInterface $client */
+    $client = $token_entity->getClient();
+    $client_drupal_entity = $client->getDrupalEntity();
+
+    return [
+      'auth_user_id' => ['target_id' => $token_entity->getUserIdentifier()],
+      'client' => ['target_id' => $client_drupal_entity->id()],
+      'scopes' => $scopes,
+      'value' => $token_entity->getIdentifier(),
+      'expire' => $token_entity->getExpiryDateTime()->format('U'),
+      'status' => TRUE,
+    ];
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Normalizer/TokenEntityNormalizerInterface.php b/drupal/modules/contrib/simple_oauth/src/Normalizer/TokenEntityNormalizerInterface.php
new file mode 100644
index 0000000..41ac9e6
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Normalizer/TokenEntityNormalizerInterface.php
@@ -0,0 +1,7 @@
+<?php
+
+namespace Drupal\simple_oauth\Normalizer;
+
+use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
+
+interface TokenEntityNormalizerInterface extends NormalizerInterface {}
diff --git a/drupal/modules/contrib/simple_oauth/src/Oauth2TokenListBuilder.php b/drupal/modules/contrib/simple_oauth/src/Oauth2TokenListBuilder.php
new file mode 100644
index 0000000..bfa4a27
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Oauth2TokenListBuilder.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace Drupal\simple_oauth;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityListBuilder;
+use Drupal\Core\Link;
+use Drupal\user\RoleInterface;
+
+/**
+ * Defines a class to build a listing of Access Token entities.
+ *
+ * @ingroup simple_oauth
+ */
+class Oauth2TokenListBuilder extends EntityListBuilder {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildHeader() {
+    $header['id'] = $this->t('ID');
+    $header['type'] = $this->t('Type');
+    $header['user'] = $this->t('User');
+    $header['name'] = $this->t('Token');
+    $header['client'] = $this->t('Client');
+    $header['scopes'] = $this->t('Scopes');
+    return $header + parent::buildHeader();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildRow(EntityInterface $entity) {
+    /* @var $entity \Drupal\simple_oauth\Entity\Oauth2Token */
+    $row['id'] = $entity->id();
+    $row['type'] = $entity->bundle();
+    $row['user'] = NULL;
+    $row['name'] = $entity->toLink(sprintf('%s…', substr($entity->label(), 0, 10)));
+    $row['client'] = NULL;
+    $row['scopes'] = NULL;
+    if (($user = $entity->get('auth_user_id')) && $user->entity) {
+      $row['user'] = $user->entity->toLink($user->entity->label());
+    }
+    if (($client = $entity->get('client')) && $client->entity) {
+      $row['client'] = $client->entity->toLink($client->entity->label(), 'edit-form');
+    }
+    /** @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $scopes */
+    if ($scopes = $entity->get('scopes')) {
+      $row['scopes'] = implode(', ', array_map(function (RoleInterface $role) {
+        return $role->label();
+      }, $scopes->referencedEntities()));
+    }
+
+    return $row + parent::buildRow($entity);
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/PageCache/DisallowSimpleOauthRequests.php b/drupal/modules/contrib/simple_oauth/src/PageCache/DisallowSimpleOauthRequests.php
new file mode 100644
index 0000000..8e24255
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/PageCache/DisallowSimpleOauthRequests.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Drupal\simple_oauth\PageCache;
+
+use Drupal\Core\PageCache\RequestPolicyInterface;
+use Drupal\simple_oauth\Authentication\Provider\SimpleOauthAuthenticationProvider;
+use Drupal\simple_oauth\Server\ResourceServerInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * @internal
+ */
+class DisallowSimpleOauthRequests implements RequestPolicyInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function check(Request $request) {
+    return SimpleOauthAuthenticationProvider::hasTokenValue($request) ? self::DENY : NULL;
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Plugin/Oauth2Grant/Password.php b/drupal/modules/contrib/simple_oauth/src/Plugin/Oauth2Grant/Password.php
new file mode 100644
index 0000000..38e588e
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Plugin/Oauth2Grant/Password.php
@@ -0,0 +1,72 @@
+<?php
+
+
+namespace Drupal\simple_oauth\Plugin\Oauth2Grant;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\simple_oauth\Plugin\Oauth2GrantBase;
+use League\OAuth2\Server\Grant\PasswordGrant;
+use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
+use League\OAuth2\Server\Repositories\UserRepositoryInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * @Oauth2Grant(
+ *   id = "password",
+ *   label = @Translation("Password")
+ * )
+ */
+class Password extends Oauth2GrantBase {
+
+  /**
+   * @var \League\OAuth2\Server\Repositories\UserRepositoryInterface
+   */
+  protected $userRepository;
+
+  /**
+   * @var \League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface
+   */
+  protected $refreshTokenRepository;
+
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * Class constructor.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, UserRepositoryInterface $user_repository, RefreshTokenRepositoryInterface $refresh_token_repository, ConfigFactoryInterface $config_factory) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->userRepository = $user_repository;
+    $this->refreshTokenRepository = $refresh_token_repository;
+    $this->configFactory = $config_factory;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('simple_oauth.repositories.user'),
+      $container->get('simple_oauth.repositories.refresh_token'),
+      $container->get('config.factory')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getGrantType() {
+    $grant = new PasswordGrant($this->userRepository, $this->refreshTokenRepository);
+    $settings = $this->configFactory->get('simple_oauth.settings');
+    $grant->setRefreshTokenTTL(new \DateInterval(sprintf('PT%dS', $settings->get('refresh_token_expiration'))));
+    return $grant;
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Plugin/Oauth2GrantBase.php b/drupal/modules/contrib/simple_oauth/src/Plugin/Oauth2GrantBase.php
new file mode 100644
index 0000000..2879c4e
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Plugin/Oauth2GrantBase.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\simple_oauth\Plugin;
+
+use Drupal\Component\Plugin\PluginBase;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Base class for OAuth2 Grant plugins.
+ */
+abstract class Oauth2GrantBase extends PluginBase implements Oauth2GrantInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition
+    );
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Plugin/Oauth2GrantInterface.php b/drupal/modules/contrib/simple_oauth/src/Plugin/Oauth2GrantInterface.php
new file mode 100644
index 0000000..ac5c946
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Plugin/Oauth2GrantInterface.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Drupal\simple_oauth\Plugin;
+
+use Drupal\Component\Plugin\PluginInspectionInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+
+/**
+ * Defines an interface for OAuth2 Grant plugins.
+ */
+interface Oauth2GrantInterface extends PluginInspectionInterface, ContainerFactoryPluginInterface  {
+
+  /**
+   * Gets the grant object.
+   *
+   * @param League\OAuth2\Server\Grant\GrantTypeInterface
+   *   The grant type.
+   */
+  public function getGrantType();
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Plugin/Oauth2GrantManager.php b/drupal/modules/contrib/simple_oauth/src/Plugin/Oauth2GrantManager.php
new file mode 100644
index 0000000..069a044
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Plugin/Oauth2GrantManager.php
@@ -0,0 +1,146 @@
+<?php
+
+namespace Drupal\simple_oauth\Plugin;
+
+use Defuse\Crypto\Core;
+use Drupal\Component\Plugin\Exception\PluginNotFoundException;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Config\ImmutableConfig;
+use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Site\Settings;
+use League\OAuth2\Server\AuthorizationServer;
+use League\OAuth2\Server\Exception\OAuthServerException;
+use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
+use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
+use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
+use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
+
+/**
+ * Provides the OAuth2 Grant plugin manager.
+ */
+class Oauth2GrantManager extends DefaultPluginManager implements Oauth2GrantManagerInterface {
+
+  /**
+   * @var \League\OAuth2\Server\Repositories\ClientRepositoryInterface
+   */
+  protected $clientRepository;
+
+  /**
+   * @var \League\OAuth2\Server\Repositories\ScopeRepositoryInterface
+   */
+  protected $scopeRepository;
+
+  /**
+   * @var \League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface
+   */
+  protected $accessTokenRepository;
+
+  /**
+   * @var \League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface
+   */
+  protected $refreshTokenRepository;
+
+  /**
+   * @var string
+   */
+  protected $privateKeyPath;
+
+  /**
+   * @var string
+   */
+  protected $publicKeyPath;
+
+  /**
+   * @var \DateTime
+   */
+  protected $expiration;
+
+  /**
+   * Constructor for Oauth2GrantManager objects.
+   *
+   * @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,
+    ClientRepositoryInterface $client_repository,
+    ScopeRepositoryInterface $scope_repository,
+    AccessTokenRepositoryInterface $access_token_repository,
+    RefreshTokenRepositoryInterface $refresh_token_repository,
+    ConfigFactoryInterface $config_factory
+  ) {
+    parent::__construct('Plugin/Oauth2Grant', $namespaces, $module_handler, 'Drupal\simple_oauth\Plugin\Oauth2GrantInterface', 'Drupal\simple_oauth\Annotation\Oauth2Grant');
+
+    $this->alterInfo('simple_oauth_oauth2_grant_info');
+    $this->setCacheBackend($cache_backend, 'simple_oauth_oauth2_grant_plugins');
+
+    $this->clientRepository = $client_repository;
+    $this->scopeRepository = $scope_repository;
+    $this->accessTokenRepository = $access_token_repository;
+    $this->refreshTokenRepository = $refresh_token_repository;
+    $settings = $config_factory->get('simple_oauth.settings');
+    $this->setKeyPaths($settings);
+    $this->expiration = new \DateInterval(sprintf('PT%dS', $settings->get('access_token_expiration')));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getAuthorizationServer($grant_type) {
+    try {
+      /** @var \Drupal\simple_oauth\Plugin\Oauth2GrantInterface $plugin */
+      $plugin = $this->createInstance($grant_type);
+    }
+    catch (PluginNotFoundException $exception) {
+      throw OAuthServerException::invalidGrant('Check the configuration to see if the grant is enabled.');
+    }
+
+    $this->checkKeyPaths();
+    $salt = Settings::getHashSalt();
+    $server = new AuthorizationServer(
+      $this->clientRepository,
+      $this->accessTokenRepository,
+      $this->scopeRepository,
+      realpath($this->privateKeyPath),
+      Core::ourSubstr($salt, 0, 32)
+    );
+    // Enable the password grant on the server with a token TTL of X hours.
+    $server->enableGrantType(
+      $plugin->getGrantType(),
+      $this->expiration
+    );
+
+    return $server;
+  }
+
+  /**
+   * Set the public and private key paths.
+   *
+   * @param \Drupal\Core\Config\ImmutableConfig $settings
+   *   The Simple OAuth settings configuration object.
+   */
+  protected function setKeyPaths(ImmutableConfig $settings) {
+    $this->publicKeyPath = $settings->get('public_key');
+    $this->privateKeyPath = $settings->get('private_key');
+  }
+
+  /**
+   * @throws \League\OAuth2\Server\Exception\OAuthServerException
+   *   If one or both keys are not set properly.
+   */
+  protected function checkKeyPaths() {
+    if (!file_exists($this->publicKeyPath) || !file_exists($this->privateKeyPath)) {
+      throw OAuthServerException::serverError(sprintf('You need to set the OAuth2 secret and private keys.'));
+    }
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Plugin/Oauth2GrantManagerInterface.php b/drupal/modules/contrib/simple_oauth/src/Plugin/Oauth2GrantManagerInterface.php
new file mode 100644
index 0000000..02b4e20
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Plugin/Oauth2GrantManagerInterface.php
@@ -0,0 +1,22 @@
+<?php
+
+
+namespace Drupal\simple_oauth\Plugin;
+
+interface Oauth2GrantManagerInterface {
+
+  /**
+   * Gets the authorization server.
+   *
+   * @param string $grant_type
+   *   The grant type used as plugin ID.
+   *
+   * @throws \League\OAuth2\Server\Exception\OAuthServerException
+   *   When the grant cannot be found.
+   *
+   * @return \League\OAuth2\Server\AuthorizationServer
+   *   The authorization server.
+   */
+  public function getAuthorizationServer($grant_type);
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Repositories/AccessTokenRepository.php b/drupal/modules/contrib/simple_oauth/src/Repositories/AccessTokenRepository.php
new file mode 100644
index 0000000..c5bd981
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Repositories/AccessTokenRepository.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Drupal\simple_oauth\Repositories;
+
+use Drupal\simple_oauth\Entities\AccessTokenEntity;
+use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
+use League\OAuth2\Server\Entities\ClientEntityInterface;
+use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
+
+class AccessTokenRepository implements AccessTokenRepositoryInterface {
+
+  use RevocableTokenRepositoryTrait;
+
+  protected static $bundle_id = 'access_token';
+  protected static $entity_class = 'Drupal\simple_oauth\Entities\AccessTokenEntity';
+  protected static $entity_interface = 'League\OAuth2\Server\Entities\AccessTokenEntityInterface';
+
+  /**
+   * {@inheritdoc}
+   */
+  public function persistNewAccessToken(AccessTokenEntityInterface $access_token_entity) {
+    $this->persistNew($access_token_entity);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function revokeAccessToken($token_id) {
+    $this->revoke($token_id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isAccessTokenRevoked($token_id) {
+    return $this->isRevoked($token_id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getNewToken(ClientEntityInterface $client_entity, array $scopes, $user_identifier = NULL) {
+    $access_token = new AccessTokenEntity();
+    $access_token->setClient($client_entity);
+    foreach ($scopes as $scope) {
+      $access_token->addScope($scope);
+    }
+    $access_token->setUserIdentifier($user_identifier);
+
+    return $access_token;
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Repositories/ClientRepository.php b/drupal/modules/contrib/simple_oauth/src/Repositories/ClientRepository.php
new file mode 100644
index 0000000..b61c43c
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Repositories/ClientRepository.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Drupal\simple_oauth\Repositories;
+
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Password\PasswordInterface;
+use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
+use Drupal\simple_oauth\Entities\ClientEntity;
+
+class ClientRepository implements ClientRepositoryInterface {
+
+  /**
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * @var \Drupal\Core\Password\PasswordInterface
+   */
+  protected $passwordChecker;
+
+  /**
+   * Constructs a ClientRepository object.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, PasswordInterface $password_checker) {
+    $this->entityTypeManager = $entity_type_manager;
+    $this->passwordChecker = $password_checker;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getClientEntity($client_identifier, $grant_type, $client_secret = NULL, $must_validate_secret = TRUE) {
+    $client_drupal_entities = $this->entityTypeManager
+      ->getStorage('consumer')
+      ->loadByProperties(['uuid' => $client_identifier]);
+
+    // Check if the client is registered.
+    if (empty($client_drupal_entities)) {
+      return NULL;
+    }
+    /** @var \Drupal\consumers\Entity\Consumer $client_drupal_entity */
+    $client_drupal_entity = reset($client_drupal_entities);
+
+    $secret = $client_drupal_entity->get('secret')->value;
+    if (
+      $must_validate_secret && $client_drupal_entity->get('confidential')->value &&
+      $this->passwordChecker->check($client_secret, $secret) === FALSE
+    ) {
+      return NULL;
+    }
+
+    return new ClientEntity($client_drupal_entity);
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Repositories/RefreshTokenRepository.php b/drupal/modules/contrib/simple_oauth/src/Repositories/RefreshTokenRepository.php
new file mode 100644
index 0000000..c8585f1
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Repositories/RefreshTokenRepository.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Drupal\simple_oauth\Repositories;
+
+use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
+use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
+
+class RefreshTokenRepository implements RefreshTokenRepositoryInterface {
+
+  use RevocableTokenRepositoryTrait;
+
+  protected static $bundle_id = 'refresh_token';
+  protected static $entity_class = 'Drupal\simple_oauth\Entities\RefreshTokenEntity';
+  protected static $entity_interface = 'League\OAuth2\Server\Entities\RefreshTokenEntityInterface';
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getNewRefreshToken() {
+    return $this->getNew();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function persistNewRefreshToken(RefreshTokenEntityInterface $refresh_token_entity) {
+    $this->persistNew($refresh_token_entity);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function revokeRefreshToken($token_id) {
+    $this->revoke($token_id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isRefreshTokenRevoked($token_id) {
+    return $this->isRevoked($token_id);
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Repositories/RevocableTokenRepositoryTrait.php b/drupal/modules/contrib/simple_oauth/src/Repositories/RevocableTokenRepositoryTrait.php
new file mode 100644
index 0000000..fb59c4e
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Repositories/RevocableTokenRepositoryTrait.php
@@ -0,0 +1,87 @@
+<?php
+
+
+namespace Drupal\simple_oauth\Repositories;
+
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Symfony\Component\Serializer\Serializer;
+
+trait RevocableTokenRepositoryTrait {
+
+  protected static $entity_type_id = 'oauth2_token';
+
+  /**
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * @var \Symfony\Component\Serializer\Serializer
+   */
+  protected $serializer;
+
+  /**
+   * Construct a revocable token.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   * @param \Drupal\simple_oauth\Normalizer\TokenEntityNormalizerInterface $normalizer
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, Serializer $serializer) {
+    $this->entityTypeManager = $entity_type_manager;
+    $this->serializer = $serializer;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function persistNew($token_entity) {
+    if (!is_a($token_entity, static::$entity_interface)){
+      throw new \InvalidArgumentException(sprintf('%s does not implement %s.', get_class($token_entity), static::$entity_interface));
+    }
+    $values = $this->serializer->normalize($token_entity);
+    $values['bundle'] = static::$bundle_id;
+    $new_token = $this->entityTypeManager->getStorage(static::$entity_type_id)->create($values);
+    $new_token->save();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function revoke($token_id) {
+    if (!$tokens = $this
+      ->entityTypeManager
+      ->getStorage(static::$entity_type_id)
+      ->loadByProperties(['value' => $token_id])) {
+      return;
+    }
+    /** @var \Drupal\simple_oauth\Entity\Oauth2TokenInterface $token */
+    $token = reset($tokens);
+    $token->revoke();
+    $token->save();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isRevoked($token_id) {
+    if (!$tokens = $this
+      ->entityTypeManager
+      ->getStorage(static::$entity_type_id)
+      ->loadByProperties(['value' => $token_id])) {
+      return TRUE;
+    }
+    /** @var \Drupal\simple_oauth\Entity\Oauth2TokenInterface $token */
+    $token = reset($tokens);
+
+    return $token->isRevoked();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getNew() {
+    $class = static::$entity_class;
+    return new $class();
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Repositories/ScopeRepository.php b/drupal/modules/contrib/simple_oauth/src/Repositories/ScopeRepository.php
new file mode 100644
index 0000000..da47796
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Repositories/ScopeRepository.php
@@ -0,0 +1,123 @@
+<?php
+
+namespace Drupal\simple_oauth\Repositories;
+
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\user\RoleInterface;
+use League\OAuth2\Server\Entities\ClientEntityInterface;
+use League\OAuth2\Server\Entities\ScopeEntityInterface;
+use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
+use Drupal\simple_oauth\Entities\ScopeEntity;
+
+class ScopeRepository implements ScopeRepositoryInterface {
+
+  /**
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * ScopeRepository constructor.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getScopeEntityByIdentifier($scope_identifier) {
+    $role = $this->entityTypeManager
+      ->getStorage('user_role')
+      ->load($scope_identifier);
+    if (!$role) {
+      return NULL;
+    }
+
+    return $this->scopeFactory($role);
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * This will remove any role that is not associated to the identified user and
+   * add all the roles configured in the client.
+   */
+  public function finalizeScopes(array $scopes, $grant_type, ClientEntityInterface $client_entity, $user_identifier = NULL) {
+    $default_user = NULL;
+    try {
+      $default_user = $client_entity->getDrupalEntity()->get('user_id')->entity;
+    }
+    catch (\InvalidArgumentException $e) {
+      // Do nothing. This means that simple_oauth_extras is not enabled.
+    }
+    /** @var \Drupal\user\UserInterface $user */
+    $user = $user_identifier
+      ? $this->entityTypeManager->getStorage('user')->load($user_identifier)
+      : $default_user;
+    if (!$user) {
+      return [];
+    }
+
+    $role_ids = $user->getRoles();
+    // Given a user, only allow the roles that the user already has, regardless
+    // of what has been requested.
+    $scopes = array_filter($scopes, function (ScopeEntityInterface $scope) use ($role_ids) {
+      return in_array($scope->getIdentifier(), $role_ids);
+    });
+
+    // Make sure that the Authenticated role is added as well.
+    $scopes = $this->addRoleToScopes($scopes, RoleInterface::AUTHENTICATED_ID);
+    // Make sure that the client roles are added to the scopes as well.
+    /** @var \Drupal\consumers\Entity\Consumer $client_drupal_entity */
+    $client_drupal_entity = $client_entity->getDrupalEntity();
+    $scopes = array_reduce($client_drupal_entity->get('roles')->getValue(), function ($scopes, $role_id) {
+      return $this->addRoleToScopes($scopes, $role_id['target_id']);
+    }, $scopes);
+
+    return $scopes;
+  }
+
+  /**
+   * Build a scope entity.
+   *
+   * @param \Drupal\user\RoleInterface $role
+   *   The associated role.
+   *
+   * @return \League\OAuth2\Server\Entities\ScopeEntityInterface
+   *   The initialized scope entity.
+   */
+  protected function scopeFactory(RoleInterface $role) {
+    return new ScopeEntity($role);
+  }
+
+  /**
+   * Add an additional scope if it's not present.
+   *
+   * @param \League\OAuth2\Server\Entities\ScopeEntityInterface[] $scopes
+   *   The list of scopes.
+   * @param string $additional_role_id
+   *   The role ID to add as a scope.
+   *
+   * @return \League\OAuth2\Server\Entities\ScopeEntityInterface[]
+   *   The modified list of scopes.
+   */
+  protected function addRoleToScopes(array $scopes, $additional_role_id) {
+    $role_storage = $this->entityTypeManager->getStorage('user_role');
+    // Only add the role if it's not already in the list.
+    $found = array_filter($scopes, function (ScopeEntityInterface $scope) use ($additional_role_id) {
+      return $scope->getIdentifier() == $additional_role_id;
+    });
+    if (empty($found)) {
+      // If it's not there, then add the authenticated role.
+      $additional_role = $role_storage->load($additional_role_id);
+      array_push($scopes, $this->scopeFactory($additional_role));
+    }
+
+    return $scopes;
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Repositories/UserRepository.php b/drupal/modules/contrib/simple_oauth/src/Repositories/UserRepository.php
new file mode 100644
index 0000000..5e27f9c
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Repositories/UserRepository.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Drupal\simple_oauth\Repositories;
+
+use Drupal\user\UserAuthInterface;
+use League\OAuth2\Server\Entities\ClientEntityInterface;
+use League\OAuth2\Server\Repositories\UserRepositoryInterface;
+use Drupal\simple_oauth\Entities\UserEntity;
+
+class UserRepository implements UserRepositoryInterface {
+
+  /**
+   * @var \Drupal\user\UserAuthInterface
+   */
+  protected $userAuth;
+
+  /**
+   * UserRepository constructor.
+   *
+   * @param \Drupal\user\UserAuthInterface $user_auth
+   *   The service to check the user authentication.
+   */
+  public function __construct(UserAuthInterface $user_auth) {
+    $this->userAuth = $user_auth;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getUserEntityByUserCredentials($username, $password, $grantType, ClientEntityInterface $clientEntity) {
+    // TODO: Use authenticateWithFloodProtection when #2825084 lands.
+    if ($uid = $this->userAuth->authenticate($username, $password)) {
+      $user = new UserEntity();
+      $user->setIdentifier($uid);
+
+      return $user;
+    }
+
+    return NULL;
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Server/ResourceServer.php b/drupal/modules/contrib/simple_oauth/src/Server/ResourceServer.php
new file mode 100644
index 0000000..e3d51d8
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Server/ResourceServer.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace Drupal\simple_oauth\Server;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use League\OAuth2\Server\Exception\OAuthServerException;
+use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
+use League\OAuth2\Server\ResourceServer as LeageResourceServer;
+use Symfony\Bridge\PsrHttpMessage\HttpFoundationFactoryInterface;
+use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+class ResourceServer implements ResourceServerInterface {
+
+  /**
+   * @var \League\OAuth2\Server\ResourceServer
+   */
+  protected $subject;
+
+  /**
+   * @var \Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface
+   */
+  protected $messageFactory;
+
+  /**
+   * @var \Symfony\Bridge\PsrHttpMessage\HttpFoundationFactoryInterface
+   */
+  protected $foundationFactory;
+
+  /**
+   * ResourceServer constructor.
+   */
+  public function __construct(
+    AccessTokenRepositoryInterface $access_token_repository,
+    ConfigFactoryInterface $config_factory,
+    HttpMessageFactoryInterface $message_factory,
+    HttpFoundationFactoryInterface $foundation_factory
+  ) {
+    try {
+      if ($public_key = $config_factory->get('simple_oauth.settings')->get('public_key')) {
+        $this->subject = new LeageResourceServer(
+          $access_token_repository,
+          realpath($public_key)
+        );
+      }
+    }
+    catch (\LogicException $exception) {}
+    $this->messageFactory = $message_factory;
+    $this->foundationFactory = $foundation_factory;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateAuthenticatedRequest(Request $request) {
+    // Create a PSR-7 message from the request that is compatible with the OAuth
+    // library.
+    $psr7_request = $this->messageFactory->createRequest($request);
+    // Augment the request with the access token's decoded data or throw an
+    // exception if authentication is unsuccessful.
+    $output_psr7_request = $this
+      ->subject
+      ->validateAuthenticatedRequest($psr7_request);
+
+    // Convert back to the Drupal/Symfony HttpFoundation objects.
+    return $this->foundationFactory->createRequest($output_psr7_request);
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/src/Server/ResourceServerInterface.php b/drupal/modules/contrib/simple_oauth/src/Server/ResourceServerInterface.php
new file mode 100644
index 0000000..d50204f
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/src/Server/ResourceServerInterface.php
@@ -0,0 +1,23 @@
+<?php
+
+
+namespace Drupal\simple_oauth\Server;
+
+
+use Symfony\Component\HttpFoundation\Request;
+
+interface ResourceServerInterface {
+
+  /**
+   * Determine the access token validity.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object.
+   *
+   * @throws \League\OAuth2\Server\Exception\OAuthServerException
+   *
+   * @return \Symfony\Component\HttpFoundation\Request
+   *   The request object augmented with the token information.
+   */
+  public function validateAuthenticatedRequest(Request $request);
+}
diff --git a/drupal/modules/contrib/simple_oauth/tests/certificates/private.key b/drupal/modules/contrib/simple_oauth/tests/certificates/private.key
new file mode 100644
index 0000000..ae7f8f8
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/tests/certificates/private.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAvPQBbfIu1fZ9Oq/af+KAxnhMRi3BJA9qBqsXLtNUgtkf68wn
+8z484j/yj9wLRP49b0K41yoExQ8KUD1D2mSh9C45GCmeBD4dM8KNMs2flSAXFgIV
+twABuu+7k+75RIndJo33heADIYf6BKT1Q4nAgDi4pyfvDYjYp5iDyeLNcWiNUo/Y
+Y4aKoDH36plUPA+kP1ekjCCPw7jsnV50zvCPbutvO7TZAEve/3SUIqxs0L6eG6Zv
+PV2hWAqItXpXiy/WMbtkCjlwGTb60yKmjkAUNyAppSPnclH3h6HdtOzVjXfWkO9H
+x3C4OAL7QET3/arRt1GDiWKwfc+Dv04lXDT0AwIDAQABAoIBAGOHgA1C6YrI2LQG
+F2kPjVd93GeHCFqPSAEVNBP1O2nlJtxU4KJPIVDn8EP423LPHNszYRvtRS/ruToE
+2235Xhm6E1b37QU9FrLCAxBEoY+ypJZyKLAJb9/hEYRd960zlWsOkthQ5DVQY9D4
+dzzJHb4soo9iCJivgbfeLWU1c5QNXztUoHZA0zYHbVlfNCvD7cTGJpnnPdZJLd93
+lZ1abkBAz6/WMavHnNNKBxPH/8hE8wLaaOZpwqce/RpcJlKM91db6OfrWnpj/Hrd
+XJIKbQErrJTXOlBm27+9xoX+btg1GR9JowlUZ+BGoSmO+j0wqVWzHb+NSkbMPf10
+uLyE8QkCgYEA7IDz5AYlHvafjwzWWYvU4WHl/wJ0ZbdJfBxT2QVxu37nOzGAJJck
+EYIWtXPSOUE/eTJHzyBhycjkuQtt+/Rprxj+Sn4dpFpCDxKs+gNyI6A+MzdHdEYJ
+YarBC2M43j8psgUiYkMpfoIgiZac/qprmgmB39u9tD+4vjZKdauw2B8CgYEAzIeS
+NjXYTKaUIJYP0y1oN0eyoNfbs4h8fXRjAQUzEj1mSs0ureosTLF4lCnOKzkkVf+C
+kGpTTZ6EDn7bXxsz6/2QvnubRwzIJx+kb0UkA64623vbKnL/xM7BMt/P1Avph82r
+SS11XWesjOCRpYLGf+YE8rQJXdVf1Vr1CAJ2d50CgYBq4BtXAC/mPiz8yCBVdwtM
+jqERDFrtXFao72Q0vnEW+dIkvcnavzJddxwsA5sMpJ+6dS5eO5P1TAOQW8noAhuA
+NRs1LqjWjLMtfJMOqF/8GX4CRwjTUpMKv89dBgm85W5CNG/FV/R4ZvWtN5Lawsi9
+Y259ax/fRKyHyKD9bAkOoQKBgQDJ76i6gVs4AtgJfF/PfuuAePeyuq0ei0lujDUb
+0shj399ZR1ApQiXO6wJENyppnpdzmTyN3Yy1/CYiMbniIveWrtn0WBItij8r8Z/m
+hHtUbveJsLXpKXXCGOjDlBqcH87I2JWfQJS6ThwdU7Q5l+7oZHDKOFtvG7bs7ksz
+R0s0OQKBgH6NaykzVBPZfHrJRwdk7gfXiaLk7PfIIiiK/OyjVBbtYwaXQkUAaHEl
+Oz9H1wukAZQtqf0LEGg0qIA0UuKvtvm9Iei0KpGrz21ExbPEyFhEgp5Utmw+Oon3
+Rk3L4fDUGqyKyamiZNZSRnC6gZm87EWHQNFFqU0yZ6a/QKbpOB1W
+-----END RSA PRIVATE KEY-----
diff --git a/drupal/modules/contrib/simple_oauth/tests/certificates/public.key b/drupal/modules/contrib/simple_oauth/tests/certificates/public.key
new file mode 100644
index 0000000..11422d2
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/tests/certificates/public.key
@@ -0,0 +1,9 @@
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvPQBbfIu1fZ9Oq/af+KA
+xnhMRi3BJA9qBqsXLtNUgtkf68wn8z484j/yj9wLRP49b0K41yoExQ8KUD1D2mSh
+9C45GCmeBD4dM8KNMs2flSAXFgIVtwABuu+7k+75RIndJo33heADIYf6BKT1Q4nA
+gDi4pyfvDYjYp5iDyeLNcWiNUo/YY4aKoDH36plUPA+kP1ekjCCPw7jsnV50zvCP
+butvO7TZAEve/3SUIqxs0L6eG6ZvPV2hWAqItXpXiy/WMbtkCjlwGTb60yKmjkAU
+NyAppSPnclH3h6HdtOzVjXfWkO9Hx3C4OAL7QET3/arRt1GDiWKwfc+Dv04lXDT0
+AwIDAQAB
+-----END PUBLIC KEY-----
diff --git a/drupal/modules/contrib/simple_oauth/tests/src/Functional/PasswordFunctionalTest.php b/drupal/modules/contrib/simple_oauth/tests/src/Functional/PasswordFunctionalTest.php
new file mode 100644
index 0000000..f594480
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/tests/src/Functional/PasswordFunctionalTest.php
@@ -0,0 +1,151 @@
+<?php
+
+namespace Drupal\Tests\simple_oauth\Functional;
+
+use Drupal\Component\Serialization\Json;
+
+/**
+ * @group simple_oauth
+ */
+class PasswordFunctionalTest extends TokenBearerFunctionalTestBase {
+
+  /**
+   * @var string
+   */
+  protected $path;
+
+  /**
+   * Test the valid Password grant.
+   */
+  public function testPasswordGrant() {
+    // 1. Test the valid request.
+    $valid_payload = [
+      'grant_type' => 'password',
+      'client_id' => $this->client->uuid(),
+      'client_secret' => $this->clientSecret,
+      'username' => $this->user->getAccountName(),
+      'password' => $this->user->pass_raw,
+      'scope' => $this->scope,
+    ];
+    $response = $this->request('POST', $this->url, [
+      'form_params' => $valid_payload,
+    ]);
+    $this->assertValidTokenResponse($response, TRUE);
+
+    // 2. Test the valid request without scopes.
+    $payload_no_scope = $valid_payload;
+    unset($payload_no_scope['scope']);
+    $response = $this->request('POST', $this->url, [
+      'form_params' => $payload_no_scope,
+    ]);
+    $this->assertValidTokenResponse($response, TRUE);
+
+    // 3. Test valid request using HTTP Basic Auth.
+    $payload_no_client = $valid_payload;
+    unset($payload_no_client['client_id']);
+    unset($payload_no_client['client_secret']);
+    $response = $this->request('POST', $this->url, [
+      'form_params' => $payload_no_scope,
+      'auth' => [
+        $this->client->uuid(),
+        $this->clientSecret,
+      ],
+    ]);
+    $this->assertValidTokenResponse($response, TRUE);
+  }
+
+  /**
+   * Test invalid Password grant.
+   */
+  public function testMissingPasswordGrant() {
+    $valid_payload = [
+      'grant_type' => 'password',
+      'client_id' => $this->client->uuid(),
+      'client_secret' => $this->clientSecret,
+      'username' => $this->user->getAccountName(),
+      'password' => $this->user->pass_raw,
+      'scope' => $this->scope,
+    ];
+
+    $data = [
+      'grant_type' => [
+        'error' => 'invalid_grant',
+        'code' => 400,
+      ],
+      'client_id' => [
+        'error' => 'invalid_request',
+        'code' => 400,
+      ],
+      'client_secret' => [
+        'error' => 'invalid_client',
+        'code' => 401,
+      ],
+      'username' => [
+        'error' => 'invalid_request',
+        'code' => 400,
+      ],
+      'password' => [
+        'error' => 'invalid_request',
+        'code' => 400,
+      ],
+    ];
+    foreach ($data as $key => $value) {
+      $invalid_payload = $valid_payload;
+      unset($invalid_payload[$key]);
+      $response = $this->request('POST', $this->url, [
+        'form_params' => $invalid_payload,
+      ]);
+      $parsed_response = Json::decode($response->getBody()->getContents());
+      $this->assertSame($value['error'], $parsed_response['error'], sprintf('Correct error code %s for %s.', $value['error'], $key));
+      $this->assertSame($value['code'], $response->getStatusCode(), sprintf('Correct status code %d for %s.', $value['code'], $key));
+    }
+  }
+
+  /**
+   * Test invalid Password grant.
+   */
+  public function testInvalidPasswordGrant() {
+    $valid_payload = [
+      'grant_type' => 'password',
+      'client_id' => $this->client->uuid(),
+      'client_secret' => $this->clientSecret,
+      'username' => $this->user->getAccountName(),
+      'password' => $this->user->pass_raw,
+      'scope' => $this->scope,
+    ];
+
+    $data = [
+      'grant_type' => [
+        'error' => 'invalid_grant',
+        'code' => 400,
+      ],
+      'client_id' => [
+        'error' => 'invalid_client',
+        'code' => 401,
+      ],
+      'client_secret' => [
+        'error' => 'invalid_client',
+        'code' => 401,
+      ],
+      'username' => [
+        'error' => 'invalid_credentials',
+        'code' => 401,
+      ],
+      'password' => [
+        'error' => 'invalid_credentials',
+        'code' => 401,
+      ],
+    ];
+    foreach ($data as $key => $value) {
+      $invalid_payload = $valid_payload;
+      $invalid_payload[$key] = $this->getRandomGenerator()->string();
+      $response = $this->request('POST', $this->url, [
+        'form_params' => $invalid_payload,
+      ]);
+      $parsed_response = Json::decode($response->getBody()->getContents());
+      $this->assertSame($value['error'], $parsed_response['error'], sprintf('Correct error code %s for %s.', $value['error'], $key));
+      $this->assertSame($value['code'], $response->getStatusCode(), sprintf('Correct status code %d for %s.', $value['code'], $key));
+    }
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/tests/src/Functional/RequestHelperTrait.php b/drupal/modules/contrib/simple_oauth/tests/src/Functional/RequestHelperTrait.php
new file mode 100644
index 0000000..3ecd4b2
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/tests/src/Functional/RequestHelperTrait.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Drupal\Tests\simple_oauth\Functional;
+
+use Drupal\Core\Url;
+use GuzzleHttp\Exception\ClientException;
+use GuzzleHttp\Exception\RequestException;
+use GuzzleHttp\Exception\ServerException;
+
+trait RequestHelperTrait {
+
+  /**
+   * Performs a HTTP request. Wraps the Guzzle HTTP client.
+   *
+   * Why wrap the Guzzle HTTP client? Because any error response is returned via
+   * an exception, which would make the tests unnecessarily complex to read.
+   *
+   * @param string $method
+   *   HTTP method.
+   * @param \Drupal\Core\Url $url
+   *   URL to request.
+   * @param array $request_options
+   *   Request options to apply.
+   *
+   * @return \Psr\Http\Message\ResponseInterface
+   *   The response
+   *
+   * @see \GuzzleHttp\ClientInterface::request()
+   */
+  protected function request($method, Url $url, array $request_options) {
+    try {
+      $response = $this->httpClient->request($method, $url->toString(), $request_options);
+    }
+    catch (ClientException $e) {
+      $response = $e->getResponse();
+    }
+    catch (ServerException $e) {
+      $response = $e->getResponse();
+    }
+    catch (RequestException $e) {
+      $response = $e->getResponse();
+    }
+
+    return $response;
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/tests/src/Functional/TokenBearerFunctionalTestBase.php b/drupal/modules/contrib/simple_oauth/tests/src/Functional/TokenBearerFunctionalTestBase.php
new file mode 100644
index 0000000..6286f00
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/tests/src/Functional/TokenBearerFunctionalTestBase.php
@@ -0,0 +1,178 @@
+<?php
+
+namespace Drupal\Tests\simple_oauth\Functional;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Core\Url;
+use Drupal\consumers\Entity\Consumer;
+use Drupal\Tests\BrowserTestBase;
+use Drupal\user\Entity\Role;
+use Drupal\user\RoleInterface;
+use Psr\Http\Message\ResponseInterface;
+
+/**
+ * Class TokenBearerFunctionalTestBase
+ *
+ * Base class that handles common logic and config for the token tests.
+ *
+ * @package Drupal\Tests\simple_oauth\Functional
+ */
+abstract class TokenBearerFunctionalTestBase extends BrowserTestBase {
+
+  use RequestHelperTrait;
+
+  public static $modules = [
+    'image',
+    'node',
+    'serialization',
+    'simple_oauth',
+    'text',
+  ];
+
+  /**
+   * @var \Drupal\Core\Url
+   */
+  protected $url;
+
+  /**
+   * @var \Drupal\consumers\Entity\Consumer
+   */
+  protected $client;
+
+  /**
+   * @var \Drupal\user\UserInterface
+   */
+  protected $user;
+
+  /**
+   * @var string
+   */
+  protected $clientSecret;
+
+  /**
+   * @var \GuzzleHttp\ClientInterface
+   */
+  protected $httpClient;
+
+  /**
+   * @var \Drupal\user\RoleInterface[]
+   */
+  protected $additionalRoles;
+
+  /**
+   * @var string
+   */
+  protected $privateKeyPath;
+
+  /**
+   * @var string
+   */
+  protected $publicKeyPath;
+
+  /**
+   * @var string
+   */
+  protected $scope;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->htmlOutputEnabled = FALSE;
+
+    $this->url = Url::fromRoute('oauth2_token.token');
+
+    // Set up a HTTP client that accepts relative URLs.
+    $this->httpClient = $this->container->get('http_client_factory')
+      ->fromOptions(['base_uri' => $this->baseUrl]);
+
+    $client_role = Role::create([
+      'id' => $this->getRandomGenerator()->name(8, TRUE),
+      'label' => $this->getRandomGenerator()->word(5),
+      'is_admin' => FALSE,
+    ]);
+    $client_role->save();
+
+    $this->additionalRoles = [];
+    for ($i = 0; $i < mt_rand(1, 3); $i++) {
+      $role = Role::create([
+        'id' => $this->getRandomGenerator()->name(8, TRUE),
+        'label' => $this->getRandomGenerator()->word(5),
+        'is_admin' => FALSE,
+      ]);
+      $role->save();
+      $this->additionalRoles[] = $role;
+    }
+
+    $this->clientSecret = $this->getRandomGenerator()->string();
+
+    $this->client = Consumer::create([
+      'owner_id' => '',
+      'label' => $this->getRandomGenerator()->name(),
+      'secret' => $this->clientSecret,
+      'confidential' => TRUE,
+      'roles' => [['target_id' => $client_role->id()]],
+    ]);
+    $this->client->save();
+
+    $this->user = $this->drupalCreateUser();
+    $this->grantPermissions(Role::load(RoleInterface::ANONYMOUS_ID), [
+      'access content',
+    ]);
+    $this->grantPermissions(Role::load(RoleInterface::AUTHENTICATED_ID), [
+      'access content',
+    ]);
+
+    // Use the public and private keys.
+    $path = $this->container->get('module_handler')->getModule('simple_oauth')->getPath();
+    $temp_dir = sys_get_temp_dir();
+    $public_path = '/' . $path . '/tests/certificates/public.key';
+    $private_path = '/' . $path . '/tests/certificates/private.key';
+    file_put_contents($temp_dir . '/public.key', file_get_contents(DRUPAL_ROOT . $public_path));
+    file_put_contents($temp_dir . '/private.key', file_get_contents(DRUPAL_ROOT . $private_path));
+    chmod($temp_dir . '/public.key', 0660);
+    chmod($temp_dir . '/private.key', 0660);
+    $this->publicKeyPath = $temp_dir . '/public.key';
+    $this->privateKeyPath = $temp_dir . '/private.key';
+    $settings = $this->config('simple_oauth.settings');
+    $settings->set('public_key', $this->publicKeyPath);
+    $settings->set('private_key', $this->privateKeyPath);
+    $settings->save();
+
+    $num_roles = mt_rand(1, count($this->additionalRoles));
+    $requested_roles = array_slice($this->additionalRoles, 0, $num_roles);
+    $scopes = array_map(function (RoleInterface $role) {
+      return $role->id();
+    }, $requested_roles);
+    $this->scope = implode(' ', $scopes);
+
+    drupal_flush_all_caches();
+  }
+
+  /**
+   * Validates a valid token response.
+   *
+   * @param \Psr\Http\Message\ResponseInterface $response
+   *   The response object.
+   * @param bool $has_refresh
+   *   TRUE if the response should return a refresh token. FALSE otherwise.
+   */
+  protected function assertValidTokenResponse(ResponseInterface $response, $has_refresh = FALSE) {
+    $this->assertEquals(200, $response->getStatusCode());
+    $parsed_response = Json::decode($response->getBody()->getContents());
+    $this->assertSame('Bearer', $parsed_response['token_type']);
+    $expiration = $this->config('simple_oauth.settings')->get('access_token_expiration');
+    $this->assertLessThanOrEqual($expiration, $parsed_response['expires_in']);
+    $this->assertGreaterThanOrEqual($expiration - 10, $parsed_response['expires_in']);
+    $this->assertNotEmpty($parsed_response['access_token']);
+    if ($has_refresh) {
+      $this->assertNotEmpty($parsed_response['refresh_token']);
+    }
+    else {
+      $this->assertTrue(empty($parsed_response['refresh_token']));
+    }
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/tests/src/Unit/Authentication/Provider/SimpleOauthAuthenticationTest.php b/drupal/modules/contrib/simple_oauth/tests/src/Unit/Authentication/Provider/SimpleOauthAuthenticationTest.php
new file mode 100644
index 0000000..936969f
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/tests/src/Unit/Authentication/Provider/SimpleOauthAuthenticationTest.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Drupal\Tests\simple_oauth\Unit\Authentication\Provider;
+
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\simple_oauth\Authentication\Provider\SimpleOauthAuthenticationProvider;
+use Drupal\simple_oauth\Server\ResourceServerInterface;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * @coversDefaultClass \Drupal\simple_oauth\Authentication\Provider\SimpleOauthAuthenticationProvider
+ * @group simple_oauth
+ */
+class SimpleOauthAuthenticationTest extends UnitTestCase {
+
+  /**
+   * The authentication provider.
+   *
+   * @var \Drupal\simple_oauth\Authentication\Provider\SimpleOauthAuthenticationProviderInterface
+   */
+  protected $provider;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $resource_server = $this->prophesize(ResourceServerInterface::class);
+    $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class);
+    $this->provider = new SimpleOauthAuthenticationProvider(
+      $resource_server->reveal(),
+      $entity_type_manager->reveal()
+    );
+  }
+
+  /**
+   * @covers ::hasTokenValue
+   * @covers ::applies
+   *
+   * @dataProvider hasTokenValueProvider
+   */
+  public function testHasTokenValue(Request $request, $has_token) {
+    $this->assertSame($has_token, $this->provider->hasTokenValue($request));
+  }
+
+  public function hasTokenValueProvider() {
+    $data = [];
+
+    // 1. Authentication header.
+    $token = $this->getRandomGenerator()->name();
+    $request = new Request();
+    $request->headers->set('Authorization', 'Bearer ' . $token);
+    $data[] = [$request, TRUE];
+
+    // 2. Authentication header. Trailing white spaces.
+    $token = $this->getRandomGenerator()->name();
+    $request = new Request();
+    $request->headers->set('Authorization', '  Bearer ' . $token);
+    $data[] = [$request, TRUE];
+
+    // 3. Authentication header. No white spaces.
+    $token = $this->getRandomGenerator()->name();
+    $request = new Request();
+    $request->headers->set('Authorization', 'Foo' . $token);
+    $data[] = [$request, FALSE];
+
+    // 4. Authentication header. Fail: no token.
+    $request = new Request();
+    $data[] = [$request, FALSE];
+
+    return $data;
+  }
+
+}
diff --git a/drupal/modules/contrib/simple_oauth/tests/src/Unit/EntityCollectorTest.php b/drupal/modules/contrib/simple_oauth/tests/src/Unit/EntityCollectorTest.php
new file mode 100644
index 0000000..bad93ac
--- /dev/null
+++ b/drupal/modules/contrib/simple_oauth/tests/src/Unit/EntityCollectorTest.php
@@ -0,0 +1,117 @@
+<?php
+
+namespace Drupal\Tests\simple_oauth\Unit;
+
+use Drupal\Component\Datetime\TimeInterface;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Entity\Query\QueryInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\consumers\Entity\Consumer;
+use Drupal\simple_oauth\Entity\Oauth2Token;
+use Drupal\simple_oauth\ExpiredCollector;
+use Drupal\Tests\UnitTestCase;
+use Prophecy\Argument;
+
+/**
+ * @coversDefaultClass \Drupal\simple_oauth\ExpiredCollector
+ * @group simple_oauth
+ */
+class EntityCollectorTest extends UnitTestCase {
+
+  /**
+   * @covers ::collect
+   */
+  public function testCollect() {
+    list($expired_collector, $query,) = $this->buildProphecies();
+    $query->condition('expire', 42, '<')->shouldBeCalledTimes(1);
+    $this->assertArrayEquals([1, 52], array_map(function ($entity) {
+      return $entity->id();
+    }, $expired_collector->collect()));
+  }
+
+  /**
+   * @covers ::collectForClient
+   */
+  public function testCollectForClient() {
+    list($expired_collector, $query,) = $this->buildProphecies();
+    $client = $this->prophesize(Consumer::class);
+    $client->id()->willReturn(35);
+    $query->condition('client', 35)->shouldBeCalledTimes(1);
+    $tokens = $expired_collector->collectForClient($client->reveal());
+    $this->assertArrayEquals([1, 52], array_map(function ($entity) {
+      return $entity->id();
+    }, $tokens));
+  }
+
+  /**
+   * @covers ::collectForAccount
+   */
+  public function testCollectForAccount() {
+    list($expired_collector, $token_query,,, $client_storage) = $this->buildProphecies();
+    $account = $this->prophesize(AccountInterface::class);
+    $account->id()->willReturn(22);
+    $token_query->condition('auth_user_id', 22)->shouldBeCalledTimes(1);
+    $client_storage->loadByProperties([
+      'user_id' => 22,
+    ])->shouldBeCalledTimes(1);
+    $token_query->condition('client', 6)->shouldBeCalledTimes(1);
+    $tokens = $expired_collector->collectForAccount($account->reveal());
+    $this->assertArrayEquals([1, 52], array_map(function ($entity) {
+      return $entity->id();
+    }, $tokens));
+  }
+
+  /**
+   * @covers ::collect
+   */
+  public function testDeleteMultipleTokens() {
+    list($expired_collector,, $storage) = $this->buildProphecies();
+    $storage->delete(['foo'])->shouldBeCalledTimes(1);
+    $expired_collector->deleteMultipleTokens(['foo']);
+  }
+
+  protected function buildProphecies() {
+    $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class);
+
+    $token_storage = $this->prophesize(EntityStorageInterface::class);
+    $token_query = $this->prophesize(QueryInterface::class);
+    $token_query->execute()->willReturn([1 => '1', 52 => '52']);
+    $token_storage->getQuery()->willReturn($token_query->reveal());
+    $token1 = $this->prophesize(Oauth2Token::class);
+    $token1->id()->willReturn(1);
+    $token52 = $this->prophesize(Oauth2Token::class);
+    $token52->id()->willReturn(52);
+    $token_storage->loadMultiple(['1', '52'])->willReturn([
+      1 => $token1->reveal(),
+      52 => $token52->reveal(),
+    ]);
+
+    $client_storage = $this->prophesize(EntityStorageInterface::class);
+    $client_query = $this->prophesize(QueryInterface::class);
+    $client_query->execute()->willReturn([6 => '6']);
+    $client_storage->getQuery()->willReturn($client_query->reveal());
+    $client6 = $this->prophesize(Consumer::class);
+    $client6->id()->willReturn(6);
+    $client_storage->loadByProperties([
+      'user_id' => 22,
+    ])->willReturn([6 => $client6->reveal()]);
+
+    $entity_type_manager->getStorage('oauth2_token')->willReturn($token_storage->reveal());
+    $entity_type_manager->getStorage('consumer')->willReturn($client_storage->reveal());
+
+    $date_time = $this->prophesize(TimeInterface::class);
+    $date_time->getRequestTime()->willReturn(42);
+
+    $expired_collector = new ExpiredCollector($entity_type_manager->reveal(), $date_time->reveal());
+
+    return [
+      $expired_collector,
+      $token_query,
+      $token_storage,
+      $client_query,
+      $client_storage,
+    ];
+  }
+
+}
diff --git a/drupal/modules/custom/datacenter/admin_area_entity.page.inc b/drupal/modules/custom/datacenter/admin_area_entity.page.inc
new file mode 100755
index 0000000..957da28
--- /dev/null
+++ b/drupal/modules/custom/datacenter/admin_area_entity.page.inc
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * @file
+ * Contains admin_area_entity.page.inc.
+ *
+ * Page callback for Admin area entity entities.
+ */
+
+use Drupal\Core\Render\Element;
+
+/**
+ * Prepares variables for Admin area entity templates.
+ *
+ * Default template: admin_area_entity.html.twig.
+ *
+ * @param array $variables
+ *   An associative array containing:
+ *   - elements: An associative array containing the user information and any
+ *   - attributes: HTML attributes for the containing element.
+ */
+function template_preprocess_admin_area_entity(array &$variables) {
+  // Fetch AdminAreaEntity Entity Object.
+  $admin_area_entity = $variables['elements']['#admin_area_entity'];
+
+  // Helpful $content variable for templates.
+  foreach (Element::children($variables['elements']) as $key) {
+    $variables['content'][$key] = $variables['elements'][$key];
+  }
+}
diff --git a/drupal/modules/custom/datacenter/composer.json b/drupal/modules/custom/datacenter/composer.json
new file mode 100755
index 0000000..df15f99
--- /dev/null
+++ b/drupal/modules/custom/datacenter/composer.json
@@ -0,0 +1,14 @@
+{
+  "name": "drupal/datacenter",
+  "type": "drupal-module",
+  "description": "My entities module",
+  "keywords": ["Drupal"],
+  "license": "GPL-2.0+",
+  "homepage": "https://www.drupal.org/project/datacenter",
+  "minimum-stability": "dev",
+  "support": {
+    "issues": "https://www.drupal.org/project/issues/datacenter",
+    "source": "http://cgit.drupalcode.org/datacenter"
+  },
+  "require": { }
+}
diff --git a/drupal/modules/custom/datacenter/config/install/views.view.frontistiria.yml b/drupal/modules/custom/datacenter/config/install/views.view.frontistiria.yml
new file mode 100755
index 0000000..164ceeb
--- /dev/null
+++ b/drupal/modules/custom/datacenter/config/install/views.view.frontistiria.yml
@@ -0,0 +1,849 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - datacenter
+    - rest
+    - serialization
+id: frontistiria
+label: Frontistiria
+module: views
+description: ''
+tag: ''
+base_table: private_school_entity
+base_field: id
+core: 8.x
+display:
+  default:
+    display_plugin: default
+    id: default
+    display_title: Master
+    position: 0
+    display_options:
+      access:
+        type: none
+        options: {  }
+      cache:
+        type: tag
+        options: {  }
+      query:
+        type: views_query
+        options:
+          disable_sql_rewrite: false
+          distinct: false
+          replica: false
+          query_comment: ''
+          query_tags: {  }
+      exposed_form:
+        type: basic
+        options:
+          submit_button: Apply
+          reset_button: false
+          reset_button_label: Reset
+          exposed_sorts_label: 'Sort by'
+          expose_sort_order: true
+          sort_asc_label: Asc
+          sort_desc_label: Desc
+      pager:
+        type: mini
+        options:
+          items_per_page: 10
+          offset: 0
+          id: 0
+          total_pages: null
+          expose:
+            items_per_page: false
+            items_per_page_label: 'Items per page'
+            items_per_page_options: '5, 10, 25, 50'
+            items_per_page_options_all: false
+            items_per_page_options_all_label: '- All -'
+            offset: false
+            offset_label: Offset
+          tags:
+            previous: β€Ήβ€Ή
+            next: β€Ίβ€Ί
+      style:
+        type: serializer
+      row:
+        type: fields
+        options:
+          inline: {  }
+          separator: ''
+          hide_empty: false
+          default_field_elements: true
+      fields:
+        id_1:
+          id: id_1
+          table: private_school_entity
+          field: id
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: front_id
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: false
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: number_integer
+          settings:
+            thousand_separator: ''
+            prefix_suffix: false
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: private_school_entity
+          entity_field: id
+          plugin_id: field
+        name:
+          table: private_school_entity
+          field: name
+          id: name
+          entity_type: null
+          entity_field: name
+          plugin_id: field
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: ''
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: string
+          settings: {  }
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+        maile:
+          id: maile
+          table: private_school_entity
+          field: maile
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: ''
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: false
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: string
+          settings:
+            link_to_entity: false
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: private_school_entity
+          entity_field: maile
+          plugin_id: field
+        faxnumber:
+          id: faxnumber
+          table: private_school_entity
+          field: faxnumber
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: ''
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: false
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: string
+          settings:
+            link_to_entity: false
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: private_school_entity
+          entity_field: faxnumber
+          plugin_id: field
+        afm:
+          id: afm
+          table: private_school_entity
+          field: afm
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: ''
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: false
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: string
+          settings:
+            link_to_entity: false
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: private_school_entity
+          entity_field: afm
+          plugin_id: field
+        municipality:
+          id: municipality
+          table: private_school_entity
+          field: municipality
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: ''
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: false
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: string
+          settings:
+            link_to_entity: false
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: private_school_entity
+          entity_field: municipality
+          plugin_id: field
+        streetaddress:
+          id: streetaddress
+          table: private_school_entity
+          field: streetaddress
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: ''
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: false
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: string
+          settings:
+            link_to_entity: false
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: private_school_entity
+          entity_field: streetaddress
+          plugin_id: field
+        postalcode:
+          id: postalcode
+          table: private_school_entity
+          field: postalcode
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: ''
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: false
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: string
+          settings:
+            link_to_entity: false
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: private_school_entity
+          entity_field: postalcode
+          plugin_id: field
+        phonenumber:
+          id: phonenumber
+          table: private_school_entity
+          field: phonenumber
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: ''
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: false
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: string
+          settings:
+            link_to_entity: false
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: private_school_entity
+          entity_field: phonenumber
+          plugin_id: field
+        id:
+          id: id
+          table: admin_area_entity
+          field: id
+          relationship: adminid
+          group_type: group
+          admin_label: ''
+          label: ''
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: false
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: number_integer
+          settings:
+            thousand_separator: ''
+            prefix_suffix: false
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: admin_area_entity
+          entity_field: id
+          plugin_id: field
+        name_1:
+          id: name_1
+          table: admin_area_entity
+          field: name
+          relationship: adminid
+          group_type: group
+          admin_label: ''
+          label: ''
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: false
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: string
+          settings:
+            link_to_entity: false
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: admin_area_entity
+          entity_field: name
+          plugin_id: field
+      filters: {  }
+      sorts: {  }
+      header: {  }
+      footer: {  }
+      empty: {  }
+      relationships:
+        adminid:
+          id: adminid
+          table: private_school_entity
+          field: adminid
+          relationship: none
+          group_type: group
+          admin_label: 'Admin area entity'
+          required: false
+          entity_type: private_school_entity
+          entity_field: adminid
+          plugin_id: standard
+      arguments: {  }
+      display_extenders: {  }
+    cache_metadata:
+      max-age: -1
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - request_format
+        - url.query_args
+      tags: {  }
+  rest_export_1:
+    display_plugin: rest_export
+    id: rest_export_1
+    display_title: 'REST export'
+    position: 1
+    display_options:
+      display_extenders: {  }
+      path: frontistiria
+      pager:
+        type: none
+        options:
+          offset: 0
+      style:
+        type: serializer
+        options:
+          grouping: {  }
+          uses_fields: false
+          formats: {  }
+      row:
+        type: data_field
+        options:
+          field_options:
+            name:
+              alias: ''
+              raw_output: false
+    cache_metadata:
+      max-age: -1
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - request_format
+      tags: {  }
diff --git a/drupal/modules/custom/datacenter/config/install/views.view.get_areas.yml b/drupal/modules/custom/datacenter/config/install/views.view.get_areas.yml
new file mode 100755
index 0000000..2cca56f
--- /dev/null
+++ b/drupal/modules/custom/datacenter/config/install/views.view.get_areas.yml
@@ -0,0 +1,252 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - datacenter
+    - rest
+    - serialization
+id: get_areas
+label: 'Get areas'
+module: views
+description: ''
+tag: ''
+base_table: admin_area_entity
+base_field: id
+core: 8.x
+display:
+  default:
+    display_plugin: default
+    id: default
+    display_title: Master
+    position: 0
+    display_options:
+      access:
+        type: none
+        options: {  }
+      cache:
+        type: tag
+        options: {  }
+      query:
+        type: views_query
+        options:
+          disable_sql_rewrite: false
+          distinct: false
+          replica: false
+          query_comment: ''
+          query_tags: {  }
+      exposed_form:
+        type: basic
+        options:
+          submit_button: Apply
+          reset_button: false
+          reset_button_label: Reset
+          exposed_sorts_label: 'Sort by'
+          expose_sort_order: true
+          sort_asc_label: Asc
+          sort_desc_label: Desc
+      pager:
+        type: mini
+        options:
+          items_per_page: 10
+          offset: 0
+          id: 0
+          total_pages: null
+          expose:
+            items_per_page: false
+            items_per_page_label: 'Items per page'
+            items_per_page_options: '5, 10, 25, 50'
+            items_per_page_options_all: false
+            items_per_page_options_all_label: '- All -'
+            offset: false
+            offset_label: Offset
+          tags:
+            previous: β€Ήβ€Ή
+            next: β€Ίβ€Ί
+      style:
+        type: serializer
+      row:
+        type: fields
+        options:
+          inline: {  }
+          separator: ''
+          hide_empty: false
+          default_field_elements: true
+      fields:
+        id:
+          id: id
+          table: admin_area_entity
+          field: id
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: ''
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: false
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: number_integer
+          settings:
+            thousand_separator: ''
+            prefix_suffix: false
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: admin_area_entity
+          entity_field: id
+          plugin_id: field
+        name:
+          table: admin_area_entity
+          field: name
+          id: name
+          entity_type: null
+          entity_field: name
+          plugin_id: field
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: ''
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: string
+          settings: {  }
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+      filters: {  }
+      sorts: {  }
+      header: {  }
+      footer: {  }
+      empty: {  }
+      relationships: {  }
+      arguments: {  }
+      display_extenders: {  }
+    cache_metadata:
+      max-age: -1
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - request_format
+        - url.query_args
+      tags: {  }
+  rest_export_1:
+    display_plugin: rest_export
+    id: rest_export_1
+    display_title: 'REST export'
+    position: 1
+    display_options:
+      display_extenders: {  }
+      path: admin-areas
+      pager:
+        type: none
+        options:
+          offset: 0
+      style:
+        type: serializer
+        options:
+          grouping: {  }
+          uses_fields: false
+          formats: {  }
+      row:
+        type: data_field
+        options:
+          field_options:
+            name:
+              alias: ''
+              raw_output: false
+    cache_metadata:
+      max-age: -1
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - request_format
+      tags: {  }
diff --git a/drupal/modules/custom/datacenter/datacenter.info.yml b/drupal/modules/custom/datacenter/datacenter.info.yml
new file mode 100755
index 0000000..e04f29b
--- /dev/null
+++ b/drupal/modules/custom/datacenter/datacenter.info.yml
@@ -0,0 +1,7 @@
+name: datacenter
+type: module
+description: My entities module
+core: 8.x
+package: Custom
+dependencies:
+  - jsonb
diff --git a/drupal/modules/custom/datacenter/datacenter.links.action.yml b/drupal/modules/custom/datacenter/datacenter.links.action.yml
new file mode 100755
index 0000000..f41fbab
--- /dev/null
+++ b/drupal/modules/custom/datacenter/datacenter.links.action.yml
@@ -0,0 +1,10 @@
+entity.admin_area_entity.add_form:
+  route_name: entity.admin_area_entity.add_form
+  title: 'Add Admin area entity'
+  appears_on:
+    - entity.admin_area_entity.collection
+entity.licenses_entity.add_form:
+  route_name: entity.licenses_entity.add_form
+  title: 'Add Licenses entity'
+  appears_on:
+    - entity.licenses_entity.collection
diff --git a/drupal/modules/custom/datacenter/datacenter.links.menu.yml b/drupal/modules/custom/datacenter/datacenter.links.menu.yml
new file mode 100755
index 0000000..3661a94
--- /dev/null
+++ b/drupal/modules/custom/datacenter/datacenter.links.menu.yml
@@ -0,0 +1,27 @@
+# Admin area entity menu items definition
+entity.admin_area_entity.collection:
+  title: 'Admin area entity list'
+  route_name: entity.admin_area_entity.collection
+  description: 'List Admin area entity entities'
+  parent: system.admin_structure
+  weight: 100
+
+admin_area_entity.admin.structure.settings:
+  title: Admin area entity settings
+  description: 'Configure Admin area entity entities'
+  route_name: admin_area_entity.settings
+  parent: system.admin_structure
+
+# Licenses entity menu items definition
+entity.licenses_entity.collection:
+  title: 'Licenses entity list'
+  route_name: entity.licenses_entity.collection
+  description: 'List Licenses entity entities'
+  parent: system.admin_structure
+  weight: 100
+
+licenses_entity.admin.structure.settings:
+  title: Licenses entity settings
+  description: 'Configure Licenses entity entities'
+  route_name: licenses_entity.settings
+  parent: system.admin_structure
diff --git a/drupal/modules/custom/datacenter/datacenter.links.task.yml b/drupal/modules/custom/datacenter/datacenter.links.task.yml
new file mode 100755
index 0000000..5a1175d
--- /dev/null
+++ b/drupal/modules/custom/datacenter/datacenter.links.task.yml
@@ -0,0 +1,44 @@
+# Admin area entity routing definition
+admin_area_entity.settings_tab:
+  route_name: admin_area_entity.settings
+  title: 'Settings'
+  base_route: admin_area_entity.settings
+
+entity.admin_area_entity.canonical:
+  route_name: entity.admin_area_entity.canonical
+  base_route: entity.admin_area_entity.canonical
+  title: 'View'
+
+entity.admin_area_entity.edit_form:
+  route_name: entity.admin_area_entity.edit_form
+  base_route: entity.admin_area_entity.canonical
+  title: 'Edit'
+
+entity.admin_area_entity.delete_form:
+  route_name:  entity.admin_area_entity.delete_form
+  base_route:  entity.admin_area_entity.canonical
+  title: Delete
+  weight: 10
+
+# Licenses entity routing definition
+licenses_entity.settings_tab:
+  route_name: licenses_entity.settings
+  title: 'Settings'
+  base_route: licenses_entity.settings
+
+entity.licenses_entity.canonical:
+  route_name: entity.licenses_entity.canonical
+  base_route: entity.licenses_entity.canonical
+  title: 'View'
+
+entity.licenses_entity.edit_form:
+  route_name: entity.licenses_entity.edit_form
+  base_route: entity.licenses_entity.canonical
+  title: 'Edit'
+
+entity.licenses_entity.delete_form:
+  route_name:  entity.licenses_entity.delete_form
+  base_route:  entity.licenses_entity.canonical
+  title: Delete
+  weight: 10
+
diff --git a/drupal/modules/custom/datacenter/datacenter.module b/drupal/modules/custom/datacenter/datacenter.module
new file mode 100755
index 0000000..1dc505e
--- /dev/null
+++ b/drupal/modules/custom/datacenter/datacenter.module
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * @file
+ * Contains datacenter.module.
+ */
+
+use Drupal\Core\Routing\RouteMatchInterface;
+
+/**
+ * Implements hook_help().
+ */
+function datacenter_help($route_name, RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    // Main module help for the datacenter module.
+    case 'help.page.datacenter':
+      $output = '';
+      $output .= '<h3>' . t('About') . '</h3>';
+      $output .= '<p>' . t('My entities module') . '</p>';
+      return $output;
+
+    default:
+  }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function datacenter_theme() {
+  return [
+    'datacenter' => [
+      'render element' => 'children',
+    ],
+  ];
+}
diff --git a/drupal/modules/custom/datacenter/datacenter.permissions.yml b/drupal/modules/custom/datacenter/datacenter.permissions.yml
new file mode 100755
index 0000000..0770f37
--- /dev/null
+++ b/drupal/modules/custom/datacenter/datacenter.permissions.yml
@@ -0,0 +1,39 @@
+administer admin area entity entities:
+  title: 'Administer Admin area entity entities'
+  description: 'Allow to access the administration form to configure Admin area entity entities.'
+  restrict access: true
+
+delete admin area entity entities:
+  title: 'Delete Admin area entity entities'
+
+edit admin area entity entities:
+  title: 'Edit Admin area entity entities'
+
+access admin area entity overview:
+  title: 'Access the Admin area entity overview page'
+
+view published admin area entity entities:
+  title: 'View published Admin area entity entities'
+
+view unpublished admin area entity entities:
+  title: 'View unpublished Admin area entity entities'
+
+administer licenses entity entities:
+  title: 'Administer Licenses entity entities'
+  description: 'Allow to access the administration form to configure Licenses entity entities.'
+  restrict access: true
+
+delete licenses entity entities:
+  title: 'Delete Licenses entity entities'
+
+edit licenses entity entities:
+  title: 'Edit Licenses entity entities'
+
+access licenses entity overview:
+  title: 'Access the Licenses entity overview page'
+
+view published licenses entity entities:
+  title: 'View published Licenses entity entities'
+
+view unpublished licenses entity entities:
+  title: 'View unpublished Licenses entity entities'
diff --git a/drupal/modules/custom/datacenter/licenses_entity.page.inc b/drupal/modules/custom/datacenter/licenses_entity.page.inc
new file mode 100644
index 0000000..a9b974e
--- /dev/null
+++ b/drupal/modules/custom/datacenter/licenses_entity.page.inc
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * @file
+ * Contains licenses_entity.page.inc.
+ *
+ * Page callback for Licenses entity entities.
+ */
+
+use Drupal\Core\Render\Element;
+
+/**
+ * Prepares variables for Licenses entity templates.
+ *
+ * Default template: licenses_entity.html.twig.
+ *
+ * @param array $variables
+ *   An associative array containing:
+ *   - elements: An associative array containing the user information and any
+ *   - attributes: HTML attributes for the containing element.
+ */
+function template_preprocess_licenses_entity(array &$variables) {
+  // Fetch LicensesEntity Entity Object.
+  $licenses_entity = $variables['elements']['#licenses_entity'];
+
+  // Helpful $content variable for templates.
+  foreach (Element::children($variables['elements']) as $key) {
+    $variables['content'][$key] = $variables['elements'][$key];
+  }
+}
diff --git a/drupal/modules/custom/datacenter/src/AdminAreaEntityAccessControlHandler.php b/drupal/modules/custom/datacenter/src/AdminAreaEntityAccessControlHandler.php
new file mode 100755
index 0000000..6a3b704
--- /dev/null
+++ b/drupal/modules/custom/datacenter/src/AdminAreaEntityAccessControlHandler.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Drupal\datacenter;
+
+use Drupal\Core\Entity\EntityAccessControlHandler;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Access\AccessResult;
+
+/**
+ * Access controller for the Admin area entity entity.
+ *
+ * @see \Drupal\datacenter\Entity\AdminAreaEntity.
+ */
+class AdminAreaEntityAccessControlHandler extends EntityAccessControlHandler {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
+    /** @var \Drupal\datacenter\Entity\AdminAreaEntityInterface $entity */
+    switch ($operation) {
+      case 'view':
+        if (!$entity->isPublished()) {
+          return AccessResult::allowedIfHasPermission($account, 'view unpublished admin area entity entities');
+        }
+        return AccessResult::allowedIfHasPermission($account, 'view published admin area entity entities');
+
+      case 'update':
+        return AccessResult::allowedIfHasPermission($account, 'edit admin area entity entities');
+
+      case 'delete':
+        return AccessResult::allowedIfHasPermission($account, 'delete admin area entity entities');
+    }
+
+    // Unknown operation, no opinion.
+    return AccessResult::neutral();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
+    return AccessResult::allowedIfHasPermission($account, 'add admin area entity entities');
+  }
+
+}
diff --git a/drupal/modules/custom/datacenter/src/AdminAreaEntityHtmlRouteProvider.php b/drupal/modules/custom/datacenter/src/AdminAreaEntityHtmlRouteProvider.php
new file mode 100755
index 0000000..99e50a4
--- /dev/null
+++ b/drupal/modules/custom/datacenter/src/AdminAreaEntityHtmlRouteProvider.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Drupal\datacenter;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\Routing\AdminHtmlRouteProvider;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Provides routes for Admin area entity entities.
+ *
+ * @see Drupal\Core\Entity\Routing\AdminHtmlRouteProvider
+ * @see Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider
+ */
+class AdminAreaEntityHtmlRouteProvider extends AdminHtmlRouteProvider {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRoutes(EntityTypeInterface $entity_type) {
+    $collection = parent::getRoutes($entity_type);
+
+    $entity_type_id = $entity_type->id();
+
+    if ($collection_route = $this->getCollectionRoute($entity_type)) {
+      $collection->add("entity.{$entity_type_id}.collection", $collection_route);
+    }
+
+    if ($settings_form_route = $this->getSettingsFormRoute($entity_type)) {
+      $collection->add("$entity_type_id.settings", $settings_form_route);
+    }
+
+    return $collection;
+  }
+
+  /**
+   * Gets the collection route.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   *
+   * @return \Symfony\Component\Routing\Route|null
+   *   The generated route, if available.
+   */
+  protected function getCollectionRoute(EntityTypeInterface $entity_type) {
+    if ($entity_type->hasLinkTemplate('collection') && $entity_type->hasListBuilderClass()) {
+      $entity_type_id = $entity_type->id();
+      $route = new Route($entity_type->getLinkTemplate('collection'));
+      $route
+        ->setDefaults([
+          '_entity_list' => $entity_type_id,
+          '_title' => "{$entity_type->getLabel()} list",
+        ])
+        ->setRequirement('_permission', 'access admin area entity overview')
+        ->setOption('_admin_route', TRUE);
+
+      return $route;
+    }
+  }
+
+  /**
+   * Gets the settings form route.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   *
+   * @return \Symfony\Component\Routing\Route|null
+   *   The generated route, if available.
+   */
+  protected function getSettingsFormRoute(EntityTypeInterface $entity_type) {
+    if (!$entity_type->getBundleEntityType()) {
+      $route = new Route("/admin/structure/{$entity_type->id()}/settings");
+      $route
+        ->setDefaults([
+          '_form' => 'Drupal\datacenter\Form\AdminAreaEntitySettingsForm',
+          '_title' => "{$entity_type->getLabel()} settings",
+        ])
+        ->setRequirement('_permission', $entity_type->getAdminPermission())
+        ->setOption('_admin_route', TRUE);
+
+      return $route;
+    }
+  }
+
+}
diff --git a/drupal/modules/custom/datacenter/src/AdminAreaEntityListBuilder.php b/drupal/modules/custom/datacenter/src/AdminAreaEntityListBuilder.php
new file mode 100755
index 0000000..a151ce6
--- /dev/null
+++ b/drupal/modules/custom/datacenter/src/AdminAreaEntityListBuilder.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Drupal\datacenter;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityListBuilder;
+use Drupal\Core\Link;
+
+/**
+ * Defines a class to build a listing of Admin area entity entities.
+ *
+ * @ingroup datacenter
+ */
+class AdminAreaEntityListBuilder extends EntityListBuilder {
+
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildHeader() {
+    $header['id'] = $this->t('Admin area entity ID');
+    $header['name'] = $this->t('Name');
+    return $header + parent::buildHeader();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildRow(EntityInterface $entity) {
+    /* @var $entity \Drupal\datacenter\Entity\AdminAreaEntity */
+    $row['id'] = $entity->id();
+    $row['name'] = Link::createFromRoute(
+      $entity->label(),
+      'entity.admin_area_entity.edit_form',
+      ['admin_area_entity' => $entity->id()]
+    );
+    return $row + parent::buildRow($entity);
+  }
+
+}
diff --git a/drupal/modules/custom/datacenter/src/Entity/AdminAreaEntity.php b/drupal/modules/custom/datacenter/src/Entity/AdminAreaEntity.php
new file mode 100755
index 0000000..4697bdb
--- /dev/null
+++ b/drupal/modules/custom/datacenter/src/Entity/AdminAreaEntity.php
@@ -0,0 +1,267 @@
+<?php
+
+namespace Drupal\datacenter\Entity;
+
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Entity\ContentEntityBase;
+use Drupal\Core\Entity\EntityChangedTrait;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\user\UserInterface;
+
+/**
+ * Defines the Admin area entity entity.
+ *
+ * @ingroup datacenter
+ *
+ * @ContentEntityType(
+ *   id = "admin_area_entity",
+ *   label = @Translation("Admin area entity"),
+ *   handlers = {
+ *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
+ *     "list_builder" = "Drupal\datacenter\AdminAreaEntityListBuilder",
+ *     "views_data" = "Drupal\datacenter\Entity\AdminAreaEntityViewsData",
+ *
+ *     "form" = {
+ *       "default" = "Drupal\datacenter\Form\AdminAreaEntityForm",
+ *       "add" = "Drupal\datacenter\Form\AdminAreaEntityForm",
+ *       "edit" = "Drupal\datacenter\Form\AdminAreaEntityForm",
+ *       "delete" = "Drupal\datacenter\Form\AdminAreaEntityDeleteForm",
+ *     },
+ *     "access" = "Drupal\datacenter\AdminAreaEntityAccessControlHandler",
+ *     "route_provider" = {
+ *       "html" = "Drupal\datacenter\AdminAreaEntityHtmlRouteProvider",
+ *     },
+ *   },
+ *   base_table = "admin_area_entity",
+ *   admin_permission = "administer admin area entity entities",
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "label" = "name",
+ *     "uuid" = "uuid",
+ *     "uid" = "user_id",
+ *     "langcode" = "langcode",
+ *     "status" = "status",
+ *   },
+ *   links = {
+ *     "canonical" = "/admin/structure/admin_area_entity/{admin_area_entity}",
+ *     "add-form" = "/admin/structure/admin_area_entity/add",
+ *     "edit-form" = "/admin/structure/admin_area_entity/{admin_area_entity}/edit",
+ *     "delete-form" = "/admin/structure/admin_area_entity/{admin_area_entity}/delete",
+ *     "collection" = "/admin/structure/admin_area_entity",
+ *   },
+ *   field_ui_base_route = "admin_area_entity.settings"
+ * )
+ */
+class AdminAreaEntity extends ContentEntityBase implements AdminAreaEntityInterface {
+
+  use EntityChangedTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function preCreate(EntityStorageInterface $storage_controller, array &$values) {
+    parent::preCreate($storage_controller, $values);
+    $values += [
+      'user_id' => \Drupal::currentUser()->id(),
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getName() {
+    return $this->get('name')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setName($name) {
+    $this->set('name', $name);
+    return $this;
+  }
+
+    /**
+   * {@inheritdoc}
+   */
+  public function getRegistryNo() {
+    return $this->get('registryno')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setRegistryNo($registryno) {
+    $this->set('registryno', $registryno);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCreatedTime() {
+    return $this->get('created')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setCreatedTime($timestamp) {
+    $this->set('created', $timestamp);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOwner() {
+    return $this->get('user_id')->entity;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOwnerId() {
+    return $this->get('user_id')->target_id;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setOwnerId($uid) {
+    $this->set('user_id', $uid);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setOwner(UserInterface $account) {
+    $this->set('user_id', $account->id());
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isPublished() {
+    return (bool) $this->getEntityKey('status');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setPublished($published) {
+    $this->set('status', $published ? TRUE : FALSE);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+    $fields = parent::baseFieldDefinitions($entity_type);
+
+    $fields['user_id'] = BaseFieldDefinition::create('entity_reference')
+      ->setLabel(t('Authored by'))
+      ->setDescription(t('The user ID of author of the Admin area entity entity.'))
+      ->setRevisionable(TRUE)
+      ->setSetting('target_type', 'user')
+      ->setSetting('handler', 'default')
+      ->setTranslatable(TRUE)
+      ->setDisplayOptions('view', [
+        'label' => 'hidden',
+        'type' => 'author',
+        'weight' => 0,
+      ])
+      ->setDisplayOptions('form', [
+        'type' => 'entity_reference_autocomplete',
+        'weight' => 5,
+        'settings' => [
+          'match_operator' => 'CONTAINS',
+          'size' => '60',
+          'autocomplete_type' => 'tags',
+          'placeholder' => '',
+        ],
+      ])
+      ->setDisplayConfigurable('form', TRUE)
+      ->setDisplayConfigurable('view', TRUE);
+
+    $fields['name'] = BaseFieldDefinition::create('string')
+      ->setLabel(t('Name'))
+      ->setDescription(t('The name of the Admin area entity entity.'))
+      ->setSettings([
+        'max_length' => 50,
+        'text_processing' => 0,
+      ])
+      ->setDefaultValue('')
+      ->setDisplayOptions('view', [
+        'label' => 'above',
+        'type' => 'string',
+        'weight' => -4,
+      ])
+      ->setDisplayOptions('form', [
+        'type' => 'string_textfield',
+        'weight' => -4,
+      ])
+      ->setDisplayConfigurable('form', TRUE)
+      ->setDisplayConfigurable('view', TRUE);
+
+      $fields['registryno'] = BaseFieldDefinition::create('string')
+        ->setLabel(t('Registry No'))
+        ->setDescription(t('The registry no of the admin area entity.'))
+        ->setSettings(array(
+          'max_length' => 50,
+          'text_processing' => 0,
+        ))
+        ->setDefaultValue('0000000')
+        ->setDisplayOptions('view', array(
+          'label' => 'above',
+          'type' => 'string',
+          'weight' => -4,
+        ))
+        ->setDisplayOptions('form', array(
+          'type' => 'string_textfield',
+          'weight' => -4,
+        ))
+        ->setDisplayConfigurable('form', TRUE)
+        ->setDisplayConfigurable('view', TRUE);
+
+     $fields['maile'] = BaseFieldDefinition::create('string')
+        ->setLabel(t('e-mail ΣχολΡίου'))
+        ->setDescription(t('Ξ”ΟŽΟƒΞ΅ το e-mail ΔιΡύθυνσης'))
+        ->setSettings(array(
+          'max_length' => 50,
+          'text_processing' => 0,
+        ))
+        ->setDefaultValue('')
+        ->setDisplayOptions('view', array(
+          'label' => 'above',
+          'type' => 'string',
+          'weight' => -4,
+        ))
+        ->setDisplayOptions('form', array(
+          'type' => 'string_textfield',
+          'weight' => -4,
+        ))
+        ->setDisplayConfigurable('form', TRUE)
+        ->setDisplayConfigurable('view', TRUE);
+    
+    $fields['status'] = BaseFieldDefinition::create('boolean')
+      ->setLabel(t('Publishing status'))
+      ->setDescription(t('A boolean indicating whether the Admin area entity is published.'))
+      ->setDefaultValue(TRUE);
+
+    $fields['created'] = BaseFieldDefinition::create('created')
+      ->setLabel(t('Created'))
+      ->setDescription(t('The time that the entity was created.'));
+
+    $fields['changed'] = BaseFieldDefinition::create('changed')
+      ->setLabel(t('Changed'))
+      ->setDescription(t('The time that the entity was last edited.'));
+
+    return $fields;
+  }
+
+}
diff --git a/drupal/modules/custom/datacenter/src/Entity/AdminAreaEntityInterface.php b/drupal/modules/custom/datacenter/src/Entity/AdminAreaEntityInterface.php
new file mode 100755
index 0000000..08d8d76
--- /dev/null
+++ b/drupal/modules/custom/datacenter/src/Entity/AdminAreaEntityInterface.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace Drupal\datacenter\Entity;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityChangedInterface;
+use Drupal\user\EntityOwnerInterface;
+
+/**
+ * Provides an interface for defining Admin area entity entities.
+ *
+ * @ingroup datacenter
+ */
+interface AdminAreaEntityInterface extends  ContentEntityInterface, EntityChangedInterface, EntityOwnerInterface {
+
+  // Add get/set methods for your configuration properties here.
+
+  /**
+   * Gets the Admin area entity name.
+   *
+   * @return string
+   *   Name of the Admin area entity.
+   */
+  public function getName();
+
+  /**
+   * Sets the Admin area entity name.
+   *
+   * @param string $name
+   *   The Admin area entity name.
+   *
+   * @return \Drupal\datacenter\Entity\AdminAreaEntityInterface
+   *   The called Admin area entity entity.
+   */
+  public function setName($name);
+
+  /**
+   * Gets the Admin area entity creation timestamp.
+   *
+   * @return int
+   *   Creation timestamp of the Admin area entity.
+   */
+  public function getCreatedTime();
+
+  /**
+   * Sets the Admin area entity creation timestamp.
+   *
+   * @param int $timestamp
+   *   The Admin area entity creation timestamp.
+   *
+   * @return \Drupal\datacenter\Entity\AdminAreaEntityInterface
+   *   The called Admin area entity entity.
+   */
+  public function setCreatedTime($timestamp);
+
+  /**
+   * Returns the Admin area entity published status indicator.
+   *
+   * Unpublished Admin area entity are only visible to restricted users.
+   *
+   * @return bool
+   *   TRUE if the Admin area entity is published.
+   */
+  public function isPublished();
+
+  /**
+   * Sets the published status of a Admin area entity.
+   *
+   * @param bool $published
+   *   TRUE to set this Admin area entity to published, FALSE to set it to unpublished.
+   *
+   * @return \Drupal\datacenter\Entity\AdminAreaEntityInterface
+   *   The called Admin area entity entity.
+   */
+  public function setPublished($published);
+
+}
diff --git a/drupal/modules/custom/datacenter/src/Entity/AdminAreaEntityViewsData.php b/drupal/modules/custom/datacenter/src/Entity/AdminAreaEntityViewsData.php
new file mode 100755
index 0000000..1df7e2c
--- /dev/null
+++ b/drupal/modules/custom/datacenter/src/Entity/AdminAreaEntityViewsData.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\datacenter\Entity;
+
+use Drupal\views\EntityViewsData;
+
+/**
+ * Provides Views data for Admin area entity entities.
+ */
+class AdminAreaEntityViewsData extends EntityViewsData {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getViewsData() {
+    $data = parent::getViewsData();
+
+    // Additional information for Views integration, such as table joins, can be
+    // put here.
+
+    return $data;
+  }
+
+}
diff --git a/drupal/modules/custom/datacenter/src/Entity/LicensesEntity.php b/drupal/modules/custom/datacenter/src/Entity/LicensesEntity.php
new file mode 100644
index 0000000..f33264c
--- /dev/null
+++ b/drupal/modules/custom/datacenter/src/Entity/LicensesEntity.php
@@ -0,0 +1,284 @@
+<?php
+
+namespace Drupal\datacenter\Entity;
+
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Entity\ContentEntityBase;
+use Drupal\Core\Entity\EntityChangedTrait;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\user\UserInterface;
+
+/**
+ * Defines the Licenses entity entity.
+ *
+ * @ingroup datacenter
+ *
+ * @ContentEntityType(
+ *   id = "licenses_entity",
+ *   label = @Translation("Licenses entity"),
+ *   handlers = {
+ *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
+ *     "list_builder" = "Drupal\datacenter\LicensesEntityListBuilder",
+ *     "views_data" = "Drupal\datacenter\Entity\LicensesEntityViewsData",
+ *
+ *     "form" = {
+ *       "default" = "Drupal\datacenter\Form\LicensesEntityForm",
+ *       "add" = "Drupal\datacenter\Form\LicensesEntityForm",
+ *       "edit" = "Drupal\datacenter\Form\LicensesEntityForm",
+ *       "delete" = "Drupal\datacenter\Form\LicensesEntityDeleteForm",
+ *     },
+ *     "access" = "Drupal\datacenter\LicensesEntityAccessControlHandler",
+ *     "route_provider" = {
+ *       "html" = "Drupal\datacenter\LicensesEntityHtmlRouteProvider",
+ *     },
+ *   },
+ *   base_table = "licenses_entity",
+ *   admin_permission = "administer licenses entity entities",
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "label" = "certCode",
+ *     "uuid" = "uuid",
+ *     "uid" = "user_id",
+ *     "langcode" = "langcode",
+ *     "status" = "status",
+ *   },
+ *   links = {
+ *     "canonical" = "/admin/structure/licenses_entity/{licenses_entity}",
+ *     "add-form" = "/admin/structure/licenses_entity/add",
+ *     "edit-form" = "/admin/structure/licenses_entity/{licenses_entity}/edit",
+ *     "delete-form" = "/admin/structure/licenses_entity/{licenses_entity}/delete",
+ *     "collection" = "/admin/structure/licenses_entity",
+ *   },
+ *   field_ui_base_route = "licenses_entity.settings"
+ * )
+ */
+class LicensesEntity extends ContentEntityBase implements LicensesEntityInterface {
+
+  use EntityChangedTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function preCreate(EntityStorageInterface $storage_controller, array &$values) {
+    parent::preCreate($storage_controller, $values);
+    $values += [
+      'user_id' => \Drupal::currentUser()->id(),
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCertCode() {
+    return $this->get('certCode')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setCertCode($certCode) {
+    $this->set('certCode', $certCode);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getJsonDetails() {
+    return $this->get('jsonDetails')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setJsonDetails($jsonDetails) {
+    $this->set('jsonDetails', $jsonDetails);
+    return $this;
+  }
+
+
+    /**
+   * {@inheritdoc}
+   */
+   public function getJsonExtra() {
+    return $this->get('jsonExtra')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setJsonExtra($jsonExtra) {
+    $this->set('jsonExtra', $jsonExtra);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCreatedTime() {
+    return $this->get('created')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setCreatedTime($timestamp) {
+    $this->set('created', $timestamp);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOwner() {
+    return $this->get('user_id')->entity;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOwnerId() {
+    return $this->get('user_id')->target_id;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setOwnerId($uid) {
+    $this->set('user_id', $uid);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setOwner(UserInterface $account) {
+    $this->set('user_id', $account->id());
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isPublished() {
+    return (bool) $this->getEntityKey('status');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setPublished($published) {
+    $this->set('status', $published ? TRUE : FALSE);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+    $fields = parent::baseFieldDefinitions($entity_type);
+
+    $fields['user_id'] = BaseFieldDefinition::create('entity_reference')
+      ->setLabel(t('Authored by'))
+      ->setDescription(t('The user ID of author of the Licenses entity entity.'))
+      ->setRevisionable(TRUE)
+      ->setSetting('target_type', 'user')
+      ->setSetting('handler', 'default')
+      ->setTranslatable(TRUE)
+      ->setDisplayOptions('view', [
+        'label' => 'hidden',
+        'type' => 'author',
+        'weight' => 0,
+      ])
+      ->setDisplayOptions('form', [
+        'type' => 'entity_reference_autocomplete',
+        'weight' => 5,
+        'settings' => [
+          'match_operator' => 'CONTAINS',
+          'size' => '60',
+          'autocomplete_type' => 'tags',
+          'placeholder' => '',
+        ],
+      ])
+      ->setDisplayConfigurable('form', TRUE)
+      ->setDisplayConfigurable('view', TRUE);
+
+    $fields['certCode'] = BaseFieldDefinition::create('string')
+      ->setLabel(t('ΞšΟ‰Ξ΄ΞΉΞΊΟŒΟ‚ αδΡίας φροντιστηρίου'))
+      ->setDescription(t('ΞšΟ‰Ξ΄ΞΉΞΊΟŒΟ‚ αδΡίας φροντιστηρίου'))
+      ->setSettings([
+        'max_length' => 50,
+        'text_processing' => 0,
+      ])
+      ->setDefaultValue('')
+      ->setDisplayOptions('view', [
+        'label' => 'above',
+        'type' => 'string',
+        'weight' => -4,
+      ])
+      ->setDisplayOptions('form', [
+        'type' => 'string_textfield',
+        'weight' => -4,
+      ])
+      ->setDisplayConfigurable('form', TRUE)
+      ->setDisplayConfigurable('view', TRUE);
+
+    $fields['jsonDetails'] = BaseFieldDefinition::create('jsonb')
+      ->setLabel(t('ΔΡδομένα ΑδΡίας'))
+      ->setDescription(t('ΔΡδομένα ΑδΡίας'))
+      ->setSettings([
+        'max_length' => 50,
+        'text_processing' => 0,
+      ])
+      ->setDefaultValue('')
+      ->setDisplayOptions('view', [
+        'label' => 'above',
+        'type' => 'string',
+        'weight' => -4,
+      ])
+      ->setDisplayOptions('form', [
+        'type' => 'string_textfield',
+        'weight' => -4,
+      ])
+      ->setDisplayConfigurable('form', TRUE)
+      ->setDisplayConfigurable('view', TRUE);
+
+    $fields['jsonExtra'] = BaseFieldDefinition::create('jsonb')
+      ->setLabel(t('EΟ€ΞΉΟ€ΟΟŒΟƒΞΈΞ΅Ο„Ξ± μΡταδΡδομένα φροντιστηρίου'))
+      ->setDescription(t('EΟ€ΞΉΟ€ΟΟŒΟƒΞΈΞ΅Ο„Ξ± μΡταδΡδομένα φροντιστηρίου'))
+      ->setSettings([
+        'max_length' => 50,
+        'text_processing' => 0,
+      ])
+      ->setDefaultValue('')
+      ->setDisplayOptions('view', [
+        'label' => 'above',
+        'type' => 'string',
+        'weight' => -4,
+      ])
+      ->setDisplayOptions('form', [
+        'type' => 'string_textfield',
+        'weight' => -4,
+      ])
+      ->setDisplayConfigurable('form', TRUE)
+      ->setDisplayConfigurable('view', TRUE);
+
+
+    $fields['status'] = BaseFieldDefinition::create('boolean')
+      ->setLabel(t('Publishing status'))
+      ->setDescription(t('A boolean indicating whether the Licenses entity is published.'))
+      ->setDefaultValue(TRUE);
+
+    $fields['created'] = BaseFieldDefinition::create('created')
+      ->setLabel(t('Created'))
+      ->setDescription(t('The time that the entity was created.'));
+
+    $fields['changed'] = BaseFieldDefinition::create('changed')
+      ->setLabel(t('Changed'))
+      ->setDescription(t('The time that the entity was last edited.'));
+
+    return $fields;
+  }
+
+}
diff --git a/drupal/modules/custom/datacenter/src/Entity/LicensesEntityInterface.php b/drupal/modules/custom/datacenter/src/Entity/LicensesEntityInterface.php
new file mode 100644
index 0000000..aa7732c
--- /dev/null
+++ b/drupal/modules/custom/datacenter/src/Entity/LicensesEntityInterface.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace Drupal\datacenter\Entity;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityChangedInterface;
+use Drupal\user\EntityOwnerInterface;
+
+/**
+ * Provides an interface for defining Licenses entity entities.
+ *
+ * @ingroup datacenter
+ */
+interface LicensesEntityInterface extends  ContentEntityInterface, EntityChangedInterface, EntityOwnerInterface {
+
+  // Add get/set methods for your configuration properties here.
+
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCertCode();
+  public function setCertCode($certCode);
+  public function getJsonDetails();
+  public function setJsonDetails($jsonDetails);
+  public function getJsonExtra();
+  public function setJsonExtra($jsonExtra);
+
+
+  /**
+   * Gets the Licenses entity creation timestamp.
+   *
+   * @return int
+   *   Creation timestamp of the Licenses entity.
+   */
+  public function getCreatedTime();
+
+  /**
+   * Sets the Licenses entity creation timestamp.
+   *
+   * @param int $timestamp
+   *   The Licenses entity creation timestamp.
+   *
+   * @return \Drupal\datacenter\Entity\LicensesEntityInterface
+   *   The called Licenses entity entity.
+   */
+  public function setCreatedTime($timestamp);
+
+  /**
+   * Returns the Licenses entity published status indicator.
+   *
+   * Unpublished Licenses entity are only visible to restricted users.
+   *
+   * @return bool
+   *   TRUE if the Licenses entity is published.
+   */
+  public function isPublished();
+
+  /**
+   * Sets the published status of a Licenses entity.
+   *
+   * @param bool $published
+   *   TRUE to set this Licenses entity to published, FALSE to set it to unpublished.
+   *
+   * @return \Drupal\datacenter\Entity\LicensesEntityInterface
+   *   The called Licenses entity entity.
+   */
+  public function setPublished($published);
+
+}
diff --git a/drupal/modules/custom/datacenter/src/Entity/LicensesEntityViewsData.php b/drupal/modules/custom/datacenter/src/Entity/LicensesEntityViewsData.php
new file mode 100644
index 0000000..109181b
--- /dev/null
+++ b/drupal/modules/custom/datacenter/src/Entity/LicensesEntityViewsData.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\datacenter\Entity;
+
+use Drupal\views\EntityViewsData;
+
+/**
+ * Provides Views data for Licenses entity entities.
+ */
+class LicensesEntityViewsData extends EntityViewsData {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getViewsData() {
+    $data = parent::getViewsData();
+
+    // Additional information for Views integration, such as table joins, can be
+    // put here.
+
+    return $data;
+  }
+
+}
diff --git a/drupal/modules/custom/datacenter/src/Form/AdminAreaEntityDeleteForm.php b/drupal/modules/custom/datacenter/src/Form/AdminAreaEntityDeleteForm.php
new file mode 100755
index 0000000..2f2cf55
--- /dev/null
+++ b/drupal/modules/custom/datacenter/src/Form/AdminAreaEntityDeleteForm.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Drupal\datacenter\Form;
+
+use Drupal\Core\Entity\ContentEntityDeleteForm;
+
+/**
+ * Provides a form for deleting Admin area entity entities.
+ *
+ * @ingroup datacenter
+ */
+class AdminAreaEntityDeleteForm extends ContentEntityDeleteForm {
+
+
+}
diff --git a/drupal/modules/custom/datacenter/src/Form/AdminAreaEntityForm.php b/drupal/modules/custom/datacenter/src/Form/AdminAreaEntityForm.php
new file mode 100755
index 0000000..4f024ac
--- /dev/null
+++ b/drupal/modules/custom/datacenter/src/Form/AdminAreaEntityForm.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Drupal\datacenter\Form;
+
+use Drupal\Core\Entity\ContentEntityForm;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Form controller for Admin area entity edit forms.
+ *
+ * @ingroup datacenter
+ */
+class AdminAreaEntityForm extends ContentEntityForm {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    /* @var $entity \Drupal\datacenter\Entity\AdminAreaEntity */
+    $form = parent::buildForm($form, $form_state);
+
+    $entity = $this->entity;
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save(array $form, FormStateInterface $form_state) {
+    $entity = &$this->entity;
+
+    $status = parent::save($form, $form_state);
+
+    switch ($status) {
+      case SAVED_NEW:
+        drupal_set_message($this->t('Created the %label Admin area entity.', [
+          '%label' => $entity->label(),
+        ]));
+        break;
+
+      default:
+        drupal_set_message($this->t('Saved the %label Admin area entity.', [
+          '%label' => $entity->label(),
+        ]));
+    }
+    $form_state->setRedirect('entity.admin_area_entity.canonical', ['admin_area_entity' => $entity->id()]);
+  }
+
+}
diff --git a/drupal/modules/custom/datacenter/src/Form/AdminAreaEntitySettingsForm.php b/drupal/modules/custom/datacenter/src/Form/AdminAreaEntitySettingsForm.php
new file mode 100755
index 0000000..e2a0ae3
--- /dev/null
+++ b/drupal/modules/custom/datacenter/src/Form/AdminAreaEntitySettingsForm.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace Drupal\datacenter\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Class AdminAreaEntitySettingsForm.
+ *
+ * @package Drupal\datacenter\Form
+ *
+ * @ingroup datacenter
+ */
+class AdminAreaEntitySettingsForm extends FormBase {
+
+  /**
+   * Returns a unique string identifying the form.
+   *
+   * @return string
+   *   The unique string identifying the form.
+   */
+  public function getFormId() {
+    return 'AdminAreaEntity_settings';
+  }
+
+  /**
+   * Form submission handler.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    // Empty implementation of the abstract submit class.
+  }
+
+  /**
+   * Defines the settings form for Admin area entity entities.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return array
+   *   Form definition array.
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form['AdminAreaEntity_settings']['#markup'] = 'Settings form for Admin area entity entities. Manage field settings here.';
+    return $form;
+  }
+
+}
diff --git a/drupal/modules/custom/datacenter/src/Form/LicensesEntityDeleteForm.php b/drupal/modules/custom/datacenter/src/Form/LicensesEntityDeleteForm.php
new file mode 100644
index 0000000..7663d9c
--- /dev/null
+++ b/drupal/modules/custom/datacenter/src/Form/LicensesEntityDeleteForm.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Drupal\datacenter\Form;
+
+use Drupal\Core\Entity\ContentEntityDeleteForm;
+
+/**
+ * Provides a form for deleting Licenses entity entities.
+ *
+ * @ingroup datacenter
+ */
+class LicensesEntityDeleteForm extends ContentEntityDeleteForm {
+
+
+}
diff --git a/drupal/modules/custom/datacenter/src/Form/LicensesEntityForm.php b/drupal/modules/custom/datacenter/src/Form/LicensesEntityForm.php
new file mode 100644
index 0000000..e8aec16
--- /dev/null
+++ b/drupal/modules/custom/datacenter/src/Form/LicensesEntityForm.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Drupal\datacenter\Form;
+
+use Drupal\Core\Entity\ContentEntityForm;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Form controller for Licenses entity edit forms.
+ *
+ * @ingroup datacenter
+ */
+class LicensesEntityForm extends ContentEntityForm {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    /* @var $entity \Drupal\datacenter\Entity\LicensesEntity */
+    $form = parent::buildForm($form, $form_state);
+
+    $entity = $this->entity;
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save(array $form, FormStateInterface $form_state) {
+    $entity = &$this->entity;
+
+    $status = parent::save($form, $form_state);
+
+    switch ($status) {
+      case SAVED_NEW:
+        drupal_set_message($this->t('Created the %label Licenses entity.', [
+          '%label' => $entity->label(),
+        ]));
+        break;
+
+      default:
+        drupal_set_message($this->t('Saved the %label Licenses entity.', [
+          '%label' => $entity->label(),
+        ]));
+    }
+    $form_state->setRedirect('entity.licenses_entity.canonical', ['licenses_entity' => $entity->id()]);
+  }
+
+}
diff --git a/drupal/modules/custom/datacenter/src/Form/LicensesEntitySettingsForm.php b/drupal/modules/custom/datacenter/src/Form/LicensesEntitySettingsForm.php
new file mode 100644
index 0000000..51279f8
--- /dev/null
+++ b/drupal/modules/custom/datacenter/src/Form/LicensesEntitySettingsForm.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace Drupal\datacenter\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Class LicensesEntitySettingsForm.
+ *
+ * @package Drupal\datacenter\Form
+ *
+ * @ingroup datacenter
+ */
+class LicensesEntitySettingsForm extends FormBase {
+
+  /**
+   * Returns a unique string identifying the form.
+   *
+   * @return string
+   *   The unique string identifying the form.
+   */
+  public function getFormId() {
+    return 'LicensesEntity_settings';
+  }
+
+  /**
+   * Form submission handler.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    // Empty implementation of the abstract submit class.
+  }
+
+  /**
+   * Defines the settings form for Licenses entity entities.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return array
+   *   Form definition array.
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form['LicensesEntity_settings']['#markup'] = 'Settings form for Licenses entity entities. Manage field settings here.';
+    return $form;
+  }
+
+}
diff --git a/drupal/modules/custom/datacenter/src/LicensesEntityAccessControlHandler.php b/drupal/modules/custom/datacenter/src/LicensesEntityAccessControlHandler.php
new file mode 100644
index 0000000..3c4faa8
--- /dev/null
+++ b/drupal/modules/custom/datacenter/src/LicensesEntityAccessControlHandler.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Drupal\datacenter;
+
+use Drupal\Core\Entity\EntityAccessControlHandler;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Access\AccessResult;
+
+/**
+ * Access controller for the Licenses entity entity.
+ *
+ * @see \Drupal\datacenter\Entity\LicensesEntity.
+ */
+class LicensesEntityAccessControlHandler extends EntityAccessControlHandler {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
+    /** @var \Drupal\datacenter\Entity\LicensesEntityInterface $entity */
+    switch ($operation) {
+      case 'view':
+        if (!$entity->isPublished()) {
+          return AccessResult::allowedIfHasPermission($account, 'view unpublished licenses entity entities');
+        }
+        return AccessResult::allowedIfHasPermission($account, 'view published licenses entity entities');
+
+      case 'update':
+        return AccessResult::allowedIfHasPermission($account, 'edit licenses entity entities');
+
+      case 'delete':
+        return AccessResult::allowedIfHasPermission($account, 'delete licenses entity entities');
+    }
+
+    // Unknown operation, no opinion.
+    return AccessResult::neutral();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
+    return AccessResult::allowedIfHasPermission($account, 'add licenses entity entities');
+  }
+
+}
diff --git a/drupal/modules/custom/datacenter/src/LicensesEntityHtmlRouteProvider.php b/drupal/modules/custom/datacenter/src/LicensesEntityHtmlRouteProvider.php
new file mode 100644
index 0000000..838b5fb
--- /dev/null
+++ b/drupal/modules/custom/datacenter/src/LicensesEntityHtmlRouteProvider.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Drupal\datacenter;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\Routing\AdminHtmlRouteProvider;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Provides routes for Licenses entity entities.
+ *
+ * @see Drupal\Core\Entity\Routing\AdminHtmlRouteProvider
+ * @see Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider
+ */
+class LicensesEntityHtmlRouteProvider extends AdminHtmlRouteProvider {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRoutes(EntityTypeInterface $entity_type) {
+    $collection = parent::getRoutes($entity_type);
+
+    $entity_type_id = $entity_type->id();
+
+    if ($collection_route = $this->getCollectionRoute($entity_type)) {
+      $collection->add("entity.{$entity_type_id}.collection", $collection_route);
+    }
+
+    if ($settings_form_route = $this->getSettingsFormRoute($entity_type)) {
+      $collection->add("$entity_type_id.settings", $settings_form_route);
+    }
+
+    return $collection;
+  }
+
+  /**
+   * Gets the collection route.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   *
+   * @return \Symfony\Component\Routing\Route|null
+   *   The generated route, if available.
+   */
+  protected function getCollectionRoute(EntityTypeInterface $entity_type) {
+    if ($entity_type->hasLinkTemplate('collection') && $entity_type->hasListBuilderClass()) {
+      $entity_type_id = $entity_type->id();
+      $route = new Route($entity_type->getLinkTemplate('collection'));
+      $route
+        ->setDefaults([
+          '_entity_list' => $entity_type_id,
+          '_title' => "{$entity_type->getLabel()} list",
+        ])
+        ->setRequirement('_permission', 'access licenses entity overview')
+        ->setOption('_admin_route', TRUE);
+
+      return $route;
+    }
+  }
+
+  /**
+   * Gets the settings form route.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   *
+   * @return \Symfony\Component\Routing\Route|null
+   *   The generated route, if available.
+   */
+  protected function getSettingsFormRoute(EntityTypeInterface $entity_type) {
+    if (!$entity_type->getBundleEntityType()) {
+      $route = new Route("/admin/structure/{$entity_type->id()}/settings");
+      $route
+        ->setDefaults([
+          '_form' => 'Drupal\datacenter\Form\LicensesEntitySettingsForm',
+          '_title' => "{$entity_type->getLabel()} settings",
+        ])
+        ->setRequirement('_permission', $entity_type->getAdminPermission())
+        ->setOption('_admin_route', TRUE);
+
+      return $route;
+    }
+  }
+
+}
diff --git a/drupal/modules/custom/datacenter/src/LicensesEntityListBuilder.php b/drupal/modules/custom/datacenter/src/LicensesEntityListBuilder.php
new file mode 100644
index 0000000..9169050
--- /dev/null
+++ b/drupal/modules/custom/datacenter/src/LicensesEntityListBuilder.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Drupal\datacenter;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityListBuilder;
+use Drupal\Core\Link;
+
+/**
+ * Defines a class to build a listing of Licenses entity entities.
+ *
+ * @ingroup datacenter
+ */
+class LicensesEntityListBuilder extends EntityListBuilder {
+
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildHeader() {
+    $header['id'] = $this->t('Licenses entity ID');
+    $header['name'] = $this->t('Name');
+    return $header + parent::buildHeader();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildRow(EntityInterface $entity) {
+    /* @var $entity \Drupal\datacenter\Entity\LicensesEntity */
+    $row['id'] = $entity->id();
+    $row['name'] = Link::createFromRoute(
+      $entity->label(),
+      'entity.licenses_entity.edit_form',
+      ['licenses_entity' => $entity->id()]
+    );
+    return $row + parent::buildRow($entity);
+  }
+
+}
diff --git a/drupal/modules/custom/datacenter/templates/admin_area_entity.html.twig b/drupal/modules/custom/datacenter/templates/admin_area_entity.html.twig
new file mode 100755
index 0000000..11df367
--- /dev/null
+++ b/drupal/modules/custom/datacenter/templates/admin_area_entity.html.twig
@@ -0,0 +1,22 @@
+{#
+/**
+ * @file admin_area_entity.html.twig
+ * Default theme implementation to present Admin area entity data.
+ *
+ * This template is used when viewing Admin area entity pages.
+ *
+ *
+ * Available variables:
+ * - content: A list of content items. Use 'content' to print all content, or
+ * - attributes: HTML attributes for the container element.
+ *
+ * @see template_preprocess_admin_area_entity()
+ *
+ * @ingroup themeable
+ */
+#}
+<div{{ attributes.addClass('admin_area_entity') }}>
+  {% if content %}
+    {{- content -}}
+  {% endif %}
+</div>
diff --git a/drupal/modules/custom/datacenter/templates/datacenter.html.twig b/drupal/modules/custom/datacenter/templates/datacenter.html.twig
new file mode 100755
index 0000000..91e43c8
--- /dev/null
+++ b/drupal/modules/custom/datacenter/templates/datacenter.html.twig
@@ -0,0 +1 @@
+<!-- Add you custom twig html here -->
\ No newline at end of file
diff --git a/drupal/modules/custom/datacenter/templates/licenses_entity.html.twig b/drupal/modules/custom/datacenter/templates/licenses_entity.html.twig
new file mode 100644
index 0000000..c28191a
--- /dev/null
+++ b/drupal/modules/custom/datacenter/templates/licenses_entity.html.twig
@@ -0,0 +1,22 @@
+{#
+/**
+ * @file licenses_entity.html.twig
+ * Default theme implementation to present Licenses entity data.
+ *
+ * This template is used when viewing Licenses entity pages.
+ *
+ *
+ * Available variables:
+ * - content: A list of content items. Use 'content' to print all content, or
+ * - attributes: HTML attributes for the container element.
+ *
+ * @see template_preprocess_licenses_entity()
+ *
+ * @ingroup themeable
+ */
+#}
+<div{{ attributes.addClass('licenses_entity') }}>
+  {% if content %}
+    {{- content -}}
+  {% endif %}
+</div>
diff --git a/drupal/modules/custom/datachanger/composer.json b/drupal/modules/custom/datachanger/composer.json
new file mode 100755
index 0000000..390c1bc
--- /dev/null
+++ b/drupal/modules/custom/datachanger/composer.json
@@ -0,0 +1,14 @@
+{
+  "name": "drupal/datachanger",
+  "type": "drupal-module",
+  "description": "My data changer",
+  "keywords": ["Drupal"],
+  "license": "GPL-2.0+",
+  "homepage": "https://www.drupal.org/project/datachanger",
+  "minimum-stability": "dev",
+  "support": {
+    "issues": "https://www.drupal.org/project/issues/datachanger",
+    "source": "http://cgit.drupalcode.org/datachanger"
+  },
+  "require": { }
+}
diff --git a/drupal/modules/custom/datachanger/datachanger.info.yml b/drupal/modules/custom/datachanger/datachanger.info.yml
new file mode 100755
index 0000000..3cb3601
--- /dev/null
+++ b/drupal/modules/custom/datachanger/datachanger.info.yml
@@ -0,0 +1,9 @@
+name: datachanger
+type: module
+description: My data changer
+core: 8.x
+package: Custom
+dependencies:
+  - datacenter
+  - simple_oauth
+  - simple_oauth_extras
\ No newline at end of file
diff --git a/drupal/modules/custom/datachanger/datachanger.module b/drupal/modules/custom/datachanger/datachanger.module
new file mode 100755
index 0000000..f73e256
--- /dev/null
+++ b/drupal/modules/custom/datachanger/datachanger.module
@@ -0,0 +1,184 @@
+<?php
+
+/**
+ * @file
+ * Contains datachanger.module.
+ */
+
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\user\Entity\User;
+
+/**
+ * Implements hook_help().
+ */
+function datachanger_help($route_name, RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    // Main module help for the datachanger module.
+    case 'help.page.datachanger':
+      $output = '';
+      $output .= '<h3>' . t('About') . '</h3>';
+      $output .= '<p>' . t('My data changer') . '</p>';
+      return $output;
+
+    default:
+  }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function datachanger_theme() {
+  return [
+    'datachanger' => [
+      'render element' => 'children',
+    ],
+  ];
+}
+
+
+
+
+/**
+ * Implements hook_install().
+ */
+
+function datachanger_install() {
+
+    $entity_manager = \Drupal::entityManager();
+    $entity_storage_admin_area = $entity_manager->getStorage('admin_area_entity');
+
+
+    //--------------CREATE ADMIN AREAS -----------------------------------------//
+    //--------------------------------------------------------------------------//
+    $arr_regions = array();
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. Ξ”Ξ‘Ξ‘ΞœΞ‘Ξ£', 'registryno'=>'0900115');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. Ξ¦Ξ©ΞšΞ™Ξ”Ξ‘Ξ£', 'registryno'=>'4800115');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. Ξ¦Ξ˜Ξ™Ξ©Ξ€Ξ™Ξ”Ξ‘Ξ£', 'registryno'=> '4600105');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. Ξ•Ξ₯Ξ‘Ξ₯΀ΑΝΙΑΣ', 'registryno'=>'130011519');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. Ξ•Ξ₯Ξ’ΞŸΞ™Ξ‘Ξ£', 'registryno'=>'1200105');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. Ξ’ΞŸΞ™Ξ©Ξ€Ξ™Ξ‘Ξ£', 'registryno'=>'0700105');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. ΞœΞ•Ξ£Ξ£Ξ—ΞΞ™Ξ‘Ξ£', 'registryno'=>'3600115');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. Ξ›Ξ‘ΞšΞ©ΞΞ™Ξ‘Ξ£', 'registryno'=>'3000115');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. ΞšΞŸΞ‘Ξ™ΞΞ˜Ξ™Ξ‘Ξ£', 'registryno'=>'2800105');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. Ξ‘Ξ‘ΞšΞ‘Ξ”Ξ™Ξ‘Ξ£', 'registryno'=>'0300105');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. Ξ‘Ξ‘Ξ“ΞŸΞ›Ξ™Ξ”Ξ‘Ξ£', 'registryno'=>'0200115');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. ΚΞ₯ΞšΞ›Ξ‘Ξ”Ξ©Ξ','registryno'=>'2900105');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. Ξ”Ξ©Ξ”Ξ•ΞšΞ‘ΞΞ—Ξ£ΞŸΞ₯', 'registryno'=>'1000105');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. ΧΑΝΙΩΝ', 'registryno'=>'5000105');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. Ξ‘Ξ•Ξ˜Ξ₯ΜΝΟΞ₯', 'registryno'=>'4100115');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. Ξ›Ξ‘Ξ£Ξ™Ξ˜Ξ™ΞŸΞ₯', 'registryno'=>'3200115');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. Ξ—Ξ‘Ξ‘ΞšΞ›Ξ•Ξ™ΞŸΞ₯', 'registryno'=>'1700105');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. Ξ§Ξ‘Ξ›ΞšΞ™Ξ”Ξ™ΞšΞ—Ξ£', 'registryno'=>'4900115');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. ΣΕΑΑΩΝ', 'registryno'=>'4400105');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. ΠΙΕΑΙΑΣ', 'registryno'=>'3900105');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. ΠΕΛΛΑΣ', 'registryno'=>'3800105');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. ΞšΞ™Ξ›ΞšΞ™Ξ£', 'registryno'=>'2600115');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. Ξ—ΞœΞ‘Ξ˜Ξ™Ξ‘Ξ£', 'registryno'=>'1600105');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. Ξ”Ξ₯Ξ€. Ξ˜Ξ•Ξ£/ΞΞ™ΞšΞ—Ξ£', 'registryno'=>'1900145');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. ΑΝΑ΀. Ξ˜Ξ•Ξ£/ΞΞ™ΞšΞ—Ξ£', 'registryno'=>'1900105');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. ΛΕΞ₯ΞšΞ‘Ξ”Ξ‘Ξ£', 'registryno'=>'3400115');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. ΞšΞ•Ξ¦Ξ‘Ξ›Ξ›Ξ—ΞΞ™Ξ‘Ξ£', 'registryno'=>'2500115');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. ΞšΞ•Ξ‘ΞšΞ₯ΑΑΣ', 'registryno'=>'2400115');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. Ξ–Ξ‘ΞšΞ₯ΝΘΟΞ₯', 'registryno'=>'1400115');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. Ξ€Ξ‘Ξ™ΞšΞ‘Ξ›Ξ©Ξ', 'registryno'=>'4500105');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. ΞœΞ‘Ξ“ΞΞ—Ξ£Ξ™Ξ‘Ξ£', 'registryno'=>'3500105');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. ΛΑΑΙΣΑΣ', 'registryno'=>'3100105');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. ΞšΞ‘Ξ‘Ξ”Ξ™Ξ€Ξ£Ξ‘Ξ£', 'registryno'=>'2200105');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. ΠΑΕΒΕΖΑΣ', 'registryno'=>'4000115');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. ΙΩΑΝΝΙΝΩΝ', 'registryno'=>'2000105');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. Ξ˜Ξ•Ξ£Ξ Ξ‘Ξ©Ξ€Ξ™Ξ‘Ξ£', 'registryno'=>'1800115');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. ΑΑ΀ΑΣ', 'registryno'=>'0400115');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. ΦΛΩΑΙΝΑΣ', 'registryno'=>'4700115');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. ΞšΞŸΞ–Ξ‘ΞΞ—Ξ£', 'registryno'=>'2700105');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. ΞšΞ‘Ξ£Ξ€ΞŸΞ‘Ξ™Ξ‘Ξ£', 'registryno'=>'2300115');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. ΓΑΕΒΕΝΩΝ', 'registryno'=>'0800115');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. ΗΛΕΙΑΣ', 'registryno'=>'1500115');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. ΑΧΑΞͺΑΣ', 'registryno'=>'0600105');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. Ξ‘Ξ™Ξ€Ξ©Ξ›ΞŸΞ‘ΞšΞ‘Ξ‘ΞΞ‘ΞΞ™Ξ‘Ξ£', 'registryno'=>'0100105');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. Ξ§Ξ™ΞŸΞ₯', 'registryno'=>'5100115');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. Ξ£Ξ‘ΞœΞŸΞ₯', 'registryno'=>'4300115');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. Ξ›Ξ•Ξ£Ξ’ΞŸΞ₯', 'registryno'=>'3300115');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. ΠΕΙΑΑΙΑ', 'registryno'=>'5200105');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. Ξ”Ξ₯Ξ€Ξ™ΞšΞ—Ξ£ Ξ‘Ξ€Ξ€Ξ™ΞšΞ—Ξ£', 'registryno'=>'0500305');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. Δ΄ Ξ‘Ξ˜Ξ—ΞΞ‘Ξ£', 'registryno'=>'0500108');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. Γ΄ Ξ‘Ξ˜Ξ—ΞΞ‘Ξ£', 'registryno'=>'0500107');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. Ξ’Ξ„ Ξ‘Ξ˜Ξ—ΞΞ‘Ξ£', 'registryno'=>'0500106');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. Ξ‘ΞΞ‘Ξ€ΞŸΞ›Ξ™ΞšΞ—Ξ£ Ξ‘Ξ€Ξ€Ξ™ΞšΞ—Ξ£', 'registryno'=>'0500205');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. Α΄ Ξ‘Ξ˜Ξ—ΞΞ‘Ξ£', 'registryno'=>'0500105');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. Ξ‘ΞŸΞ”ΞŸΞ Ξ—Ξ£', 'registryno'=>'4200115');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. ΞžΞ‘ΞΞ˜Ξ—Ξ£', 'registryno'=>'3700115');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. Ξ•Ξ’Ξ‘ΞŸΞ₯', 'registryno'=>'1100115');
+    $arr_regions[] = array('name'=>'ΔΙΕΞ₯ΘΞ₯ΝΣΗ Ξ”.Ξ•. ΞšΞ‘Ξ’Ξ‘Ξ›Ξ‘Ξ£', 'registryno'=>'2100105');
+    
+    
+    //create 58 users
+    
+    $users_array = array('0900115', '4800115','4600105', '130011519','1200105', 
+    '0700105', '3600115', '3000115','2800105', '0300105', '0200115', '2900105', 
+    '1000105', '5000105', '4100115', '3200115', '1700105', '4900115', '4400105', '3900105',
+    '3800105', '2600115', '1600105', '1900145', '1900105', '3400115', '2500115',
+    '2400115', '1400115', '4500105', '3500105', '3100105', '2200105', '4000115',
+    '2000105', '1800115', '0400115', '4700115', '2700105', '2300115',
+    '0800115', '1500115', '0600105', '0100105', '5100115', '4300115', 
+    '3300115', '5200105', '0500305', '0500108', '0500107', '0500106', '0500205',
+    '0500105', '4200115', '3700115', '1100115' ,'2100105');
+     
+    for ($i=0; $i < count($users_array) ; $i++) { 
+        // Create user object.
+        $user = User::create();
+    
+        //Mandatory settings
+        $user->setPassword($users_array[$i]);
+        $user->enforceIsNew();
+        $user->setUsername($users_array[$i]);
+        $user->addRole('area_admin'); 
+        $user->save();
+    }
+    
+    //save regions
+    $created_regions_ids = array();
+
+    foreach ($arr_regions as $key => $value) {
+        $entity_object = $entity_storage_admin_area->create($value);
+        $entity_storage_admin_area->save($entity_object);
+        $created_regions_ids[] = $entity_object->id();
+    }
+    
+
+}
+
+
+function datachanger_uninstall() {
+    //delete
+    $entity_manager = \Drupal::entityManager();
+    $entity_storage_admin_area = $entity_manager->getStorage('admin_area_entity');
+    $entity_storage_license = $entity_manager->getStorage('licenses_entity');
+
+    //diagrafi dieythynseis
+    $all_admin_areas = $entity_storage_admin_area->loadMultiple(NULL);
+    $entity_storage_admin_area->delete($all_admin_areas);
+
+    //diagrafi frontistiria license type
+    $all_frontistiria = $entity_storage_license->loadMultiple(NULL);
+    $entity_storage_license->delete($all_frontistiria);
+    
+    //delete dieythinseis deyterobathmias users
+    //delete 58 users
+    
+    $users_array = array('0900115', '4800115','4600105', '130011519','1200105', 
+    '0700105', '3600115', '3000115','2800105', '0300105', '0200115', '2900105', 
+    '1000105', '5000105', '4100115', '3200115', '1700105', '4900115', '4400105', '3900105',
+    '3800105', '2600115', '1600105', '1900145', '1900105', '3400115', '2500115',
+    '2400115', '1400115', '4500105', '3500105', '3100105', '2200105', '4000115',
+    '2000105', '1800115', '0400115', '4700115', '2700105', '2300115',
+    '0800115', '1500115', '0600105', '0100105', '5100115', '4300115', 
+    '3300115', '5200105', '0500305', '0500108', '0500107', '0500106', '0500205',
+    '0500105', '4200115', '3700115', '1100115' ,'2100105');
+   
+    for ($i=0; $i <count($users_array) ; $i++) { 
+      $userObj = user_load_by_name($users_array[$i]); 
+      user_delete($userObj->id());
+    }
+    
+
+}
diff --git a/drupal/modules/custom/datachanger/datachanger.routing.yml b/drupal/modules/custom/datachanger/datachanger.routing.yml
new file mode 100755
index 0000000..daec3fe
--- /dev/null
+++ b/drupal/modules/custom/datachanger/datachanger.routing.yml
@@ -0,0 +1,47 @@
+datachanger.data_controller_importSchoolsFromEoppep:
+  path: '/datachanger/importSchoolsFromEoppep'
+  defaults:
+    _controller: '\Drupal\datachanger\Controller\DataController::importSchoolsFromEoppep'
+    _title: 'Import schools from EOPPEP'
+  requirements:
+    _permission: 'access content'
+datachanger.data_controller_parseHtmlContent:
+  path: '/datachanger/parseHtmlContent'
+  defaults:
+    _controller: '\Drupal\datachanger\Controller\DataController::parseHtmlContent'
+    _title: 'Parse HTML Content'
+  requirements:
+    _permission: 'access content'
+datachanger.data_controller_getFrontistiria:
+  path: '/datachanger/getFrontistiria'
+  methods: [GET]
+  requirements:
+    _user_is_logged_in: 'TRUE'
+  options:
+    _auth: ['oauth2']
+    no_cache: TRUE
+  defaults:
+    _controller: '\Drupal\datachanger\Controller\DataController::getFrontistiria'
+    _title: 'Get Frontistiria'
+datachanger.data_controller_getFrontistirio:
+  path: '/datachanger/getFrontistirio'
+  methods: [GET]
+  requirements:
+    _user_is_logged_in: 'TRUE'
+  options:
+    _auth: ['oauth2']
+    no_cache: TRUE
+  defaults:
+    _controller: '\Drupal\datachanger\Controller\DataController::getFrontistirio'
+    _title: 'Get Frontistirio'
+datachanger.data_controller_getFrontistiriaByArea:
+  path: '/datachanger/getFrontistiriaByArea'
+  methods: [GET]
+  requirements:
+    _user_is_logged_in: 'TRUE'
+  options:
+    _auth: ['oauth2']
+    no_cache: TRUE
+  defaults:
+    _controller: '\Drupal\datachanger\Controller\DataController::getFrontistiriaByArea'
+    _title: 'Get Frontistiria'
\ No newline at end of file
diff --git a/drupal/modules/custom/datachanger/src/Controller/DataController.php b/drupal/modules/custom/datachanger/src/Controller/DataController.php
new file mode 100755
index 0000000..ff92272
--- /dev/null
+++ b/drupal/modules/custom/datachanger/src/Controller/DataController.php
@@ -0,0 +1,675 @@
+<?php
+
+namespace Drupal\datachanger\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\Core\Entity\EntityTypeManager;
+use Drupal\Core\Logger\LoggerChannelFactory;
+use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use GuzzleHttp\Client;
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Database\Database;
+use Drupal\Core\Database\DatabaseExceptionWrapper;
+use Drupal\Core\Database\Transaction;
+use Drupal\Core\Logger\LoggerChannelFactoryInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Drupal\user\PermissionHandlerInterface;
+use Drupal\Core\Entity\Query\QueryFactory;
+use Drupal\user\Entity\User;
+
+
+
+
+/**
+ * Class DataController.
+ *
+ * @package Drupal\datachanger\Controller
+ */
+class DataController extends ControllerBase {
+
+  protected $userPermissions;
+  /**
+   * GuzzleHttp\Client definition.
+   *
+   * @var \GuzzleHttp\Client
+   */
+  protected $httpClient;
+  /**
+   * Drupal\Core\Entity\EntityTypeManager definition.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManager
+   */
+  protected $entityTypeManager;
+  /**
+   * Drupal\Core\Logger\LoggerChannelFactory definition.
+   *
+   * @var \Drupal\Core\Logger\LoggerChannelFactory
+   */
+
+  protected $connection;
+
+  protected $logger;
+
+  protected $queryFactory;
+
+  /**
+   * Constructs a new DataController object.
+   */
+  public function __construct(QueryFactory $query_factory, EntityTypeManager $entity_type_manager, Connection $connection, Client $http_client, LoggerChannelFactory $logger_factory,
+  PermissionHandlerInterface $user_permissions) {
+    $this->queryFactory = $query_factory;
+    $this->entityTypeManager = $entity_type_manager;
+    $this->logger = $logger_factory;
+    $this->httpClient = $http_client;
+    $this->connection = $connection;
+    $this->userPermissions = $user_permissions;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity.query'),
+      $container->get('entity_type.manager'),
+      $container->get('database'),
+      $container->get('http_client'),
+      $container->get('logger.factory'),
+      $container->get('user.permissions')
+    );
+  }
+
+
+  public function importSchoolsFromEoppep() {
+    //-----------------CALL SERVICE EOPPEP -----------------------------------------//
+    //------------------------------------------------------------------------------//
+    
+    $data_string = array("licenseType" => "ΦΑΟΝ΀");
+    
+    $url = 'http://domes.eoppep.gr/home/SearchResult'; 
+    
+    $ch = curl_init();   
+    
+    curl_setopt($ch, CURLOPT_URL, $url); 
+    curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR 1.1.4322)'); 
+    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 
+    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); 
+    curl_setopt($ch, CURLOPT_TIMEOUT, 40); 
+    curl_setopt($ch, CURLOPT_POST, true); 
+    curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);
+    
+    $data = curl_exec($ch); 
+    $array = explode("<div>", $data);
+    $elements = array();
+    $final_array = array();
+    $textkey = "ΞšΟ‰Ξ΄. ΑδΡίας";
+    $whitespace = "22";
+    
+    foreach($array as $item) {
+      if( strpos($item, $textkey ) !== false ) {
+         $decode = explode(":", $item);
+         if ( strpos($decode[1], $whitespace ) !== false ) {
+             $decode2 = explode("</div>",$decode[1]);
+             $decode3 = explode("</b>",$decode2[0]);
+              $final_decode=substr($decode3[0],4);
+              array_push($elements, $final_decode);
+         }
+      }  
+    }
+    
+    $unique_array = array_unique($elements);
+    
+    for($i = 0; $i < sizeof($unique_array); $i++) {
+         if ( strpos($unique_array[$i], $whitespace ) !== false ) {
+             array_push($final_array, $unique_array[$i]);
+         }
+    }
+
+    
+    $err = curl_error($ch); 
+    
+    $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE); 
+    
+    curl_close($ch);
+    echo count($final_array);
+    exit;
+    
+
+    //--------------------------------------------------------------------------------------//
+    //------------------eisagogi  frontistirion klisi web service EOPPEP -------------------//
+    //--------------------------------------------------------------------------------------//
+    for ($i=0; $i < count($final_array); $i++) { 
+     
+      $url = 'http://domes.eoppep.gr/api/registry/getlicense?certcode='.$final_array[$i];
+ 
+      $this->httpClient = new Client(['verify' => false ]);
+
+      try{
+        $data_result = $this->httpClient->get($url);
+        if($data_result) {
+            $json_data = $data_result->getBody();
+  
+            $entity_licenses = $this->entityTypeManager->getStorage('licenses_entity');
+           
+            $arr_data = array('certCode' => $final_array[$i], 'jsonDetails' => $json_data);
+
+            $entity_object = $entity_licenses->create($arr_data);
+            $entity_licenses->save($entity_object);
+        }
+        sleep(3); 
+      }
+      catch(\Exception $e) {
+           $this->logger->warning($e->getMessage());
+      }
+    }
+  }
+
+  private function getAdminIdFromRegistryNo($registryno) {
+    $sCon = $this->connection->select('admin_area_entity','adminAreas')
+                             ->fields('adminAreas', array('id'))
+                             ->condition('adminAreas.registryno', $registryno, '=');
+    $allAreas = $sCon->execute()->fetchAll(\PDO::FETCH_OBJ);
+
+    $selectedArea = reset($allAreas);
+    return $selectedArea->id;
+  }
+
+  private function getRegistryNoFromTaxCode($taxcode) {
+ //check taxydromikous kodikous 
+   $tk_codes = array(
+    '1900105' => '57006,57019,54248,54249,54250,54351,54352,54453,54454,54500,54621,54622,54623,54624,54625,54626,54627,54628,54629,54630,54631,54632,54633,54634,54635,54636,54638,54639,54640,54641,54642,54643,54644,54645,54646,54655,55131,55132,55133,55134,55236,55535,55337,57001,57004,57010,57500,55102,56228',
+    '0500108' => '16451,16452,16561,16562,16672,16674,16675,16777,17121,17122,17123,17124,17341,17342,17343,17455,17456,17561,17562,17563,17564,17655,17671,17672,17673,17674,17675,17676,17778,18344,18345,18346,17546',
+    '0500105' => '10431,10432,10433,10434,10435,10436,10437,10438,10439,10440,10441,10442,10443,10444,10445,10446,10447,10551,10552,10553,10554,10555,10556,10557,10558,10559,10560,10561,10562,10563,10564,10671,10672,10673,10674,10675,10676,10677,10678,10679,10680,10681,10682,10683,11141,11142,11143,11144,11145,11146,11147,11251,11252,11253,11254,11255,11256,11257,11361,11362,11363,11364,11471,11472,11473,11474,11475,11476,11521,11522,11523,11524,11525,11526,11527,11528,11631,11632,11633,11634,11635,11636,11741,11742,11743,11744,11745,11851,11852,11853,11854,11855,14341,14342,14343,15771,15772,15773,16121,16122,16231,16232,16233,16341,16342,16343,16344,16345,16346,17234,17235,17236,17237,10430,14230,10689',
+    '0500205' => '16673,13671,13672,13673,13674,13675,13676,13677,13678,13679,14565,14568,14569,14572,14574,14575,14576,15344,15349,15351,15354,15374,16671,16672,16631,19001,19002,19003,19004,19005,19007,19009,19010,19011,19013,19014,19015,19016,19400,19500,19909',
+    '0500106' => '15127,15129,14121,14122,14123,14231,14232,14233,14234,14235,14451,14452,14561,14562,14563,14564,14565,14671,14578,14671,15121,15122,15123,15124,15125,15126,15231,15232,15233,15234,15235,15236,15237,15238,15341,15342,15343,15344,15451,15452,15561,15562,15669,48100',
+    '0500107' => '12351,13232,12131,12132,12133,12134,12135,12136,12137,12241,12242,12243,12244,12461,12462,13121,13122,13123,13231,13451,13561,13562,13131',
+    '0500305' => '19018,13461,13341,13342,13343,13344,13345,19006,19008,19012,19100,19200,19300,19600',
+    '5200105' => '18010,18020,18030,18040,18050,18120,18121,18122,18233,18450,18451,18452,18453,18454,18531,18532,18533,18534,18535,18536,18537,18538,18539,18540,18541,18542,18543,18544,18545,18546,18547,18755,18756,18757,18758,18648,18863,18900,18903,18902,18901,80100,80200',
+    '1900145' => '57008,53438,55438,56121,56122,56123,54628,56122,56334,56224,56429,56430,56431,56532,56533,56625,56626,56727,56728,57002,57003,57007,57009,57012,57013,57014,57015,57016,57017,57018,57020,57021,57022,57100,57200,57300,57400,57600,56535');
+    
+    $result = "";
+    $matches = array();
+    foreach($tk_codes as $k=>$v) {
+        if(preg_match("/\b$taxcode\b/i", $v)) {
+            $matches[$k] = $v;
+            $result=$k;
+        }
+    }
+    return $result;
+  }
+  
+  private function getRegistryNoFromNomos($nomosName, $taxcode) {
+    $regno = '';
+    if(!empty($nomosName)) {
+        switch ($nomosName) {
+            case 'ΞžΞ¬Ξ½ΞΈΞ·Ο‚':
+                $regno = '3700115';
+                break;
+            case 'ΞœΞ±Ξ³Ξ½Ξ·ΟƒΞ―Ξ±Ο‚':
+                $regno = '3500105';
+                break;
+            case 'Λάρισας':
+                $regno = '3100105';
+                break;   
+            case 'Ξ‘ΞΏΞ΄ΟŒΟ€Ξ·Ο‚':
+                $regno = '4200115';
+                break;
+            case 'ΗρακλΡίου':
+                $regno = '1700105';
+                break;
+            case 'Πέλλας':
+                $regno = '3800105';
+                break;
+            case 'Χανίων':
+                $regno = '5000105';
+                break;   
+            case 'Λέσβου':
+                $regno = '3300115';
+                break;
+            case 'ΞœΞ΅ΟƒΟƒΞ·Ξ½Ξ―Ξ±Ο‚':
+                $regno = '3600115';
+                break;
+            case 'ΗλΡίας':
+                $regno = '1500115';
+                break;
+            case 'ΑΡθύμνου':
+                $regno = '4100115';
+                break;   
+            case 'ΣΡρρών':
+                $regno = '4400105';
+                break;
+            case 'Ευβοίας':
+                $regno = '1200105';
+                break;
+            case 'Αιτωλοακαρνανίας':
+                $regno = '0100105';
+                break;
+            case 'ΛΡυκάδας':
+                $regno = '3400115';
+                break;   
+            case 'ΔωδΡκανΞ�σου':
+                $regno = '1000105';
+                break;
+            case 'ΞšΞ±ΟΞ΄Ξ―Ο„ΟƒΞ±Ο‚':
+                $regno = '2200105';
+                break;
+            case 'Ιωαννίνων':
+                $regno = '2000105';
+                break;
+            case 'Ξ˜Ξ΅ΟƒΟ€ΟΟ‰Ο„Ξ―Ξ±Ο‚':
+                $regno = '1800115';
+                break;   
+            case 'Αχαίας':
+                $regno = '0600105';
+                break;
+            case 'ΞšΞΉΞ»ΞΊΞ―Ο‚':
+                $regno = '2600115';
+                break;
+            case 'Ξ¦ΞΈΞΉΟŽΟ„ΞΉΞ΄Ξ±Ο‚':
+                $regno = '4600105';
+                break;   
+            case 'ΠιΡρίας':
+                $regno = '3900105';
+                break;
+            case 'Ξ—ΞΌΞ±ΞΈΞ―Ξ±Ο‚':
+                $regno = '1600105';
+                break;
+            case 'ΞšΞ±Ξ²Ξ¬Ξ»Ξ±Ο‚':
+                $regno = '2100105';
+                break;
+            case 'ΞšΞΏΟΞΉΞ½ΞΈΞ―Ξ±Ο‚':
+                $regno = '2800105';
+                break;
+            case 'Αρκαδίας':
+                $regno = '0300105';
+                break;
+            case 'Ζακύνθου':
+                $regno = '1400115';
+                break;
+            case 'Λασιθίου':
+                $regno = '3200115';
+                break;
+            case 'Ξ“ΟΞ΅Ξ²Ξ΅Ξ½ΟŽΞ½':
+                $regno = '0800115';
+                break;
+            case 'Δράμας':
+                $regno = '0900115';
+                break;
+            case 'Λακωνίας':
+                $regno = '3000115';
+                break;
+            case 'Χίου':
+                $regno = '5100115';
+                break;
+            case 'ΞˆΞ²ΟΞΏΟ…':
+                $regno = '1100115';
+                break;
+            case 'ΞšΞ΅ΟΞΊΟΟΞ±Ο‚':
+                $regno = '2400115';
+                break;
+            case 'ΧαλκιδικΞ�Ο‚':
+                $regno = '4900115';
+                break;
+            case 'ΞšΞ΅Ο†Ξ±Ξ»Ξ»ΞΏΞ½ΞΉΞ¬Ο‚':
+                $regno = '2500115';
+                break;
+            case 'Φωκίδας':
+                $regno = '4800115';
+                break;
+            case 'Ευρυτανίας':
+                $regno = '130011519';
+                break;
+            case 'Βοιωτίας':
+                $regno = '0700105';
+                break;
+            case 'Αργολίδας':
+                $regno = '0200115';
+                break;
+            case 'ΞšΟ…ΞΊΞ»Ξ¬Ξ΄Ο‰Ξ½':
+                $regno = '2900105';
+                break;
+            case '΀ρικάλων':
+                $regno = '4500105';
+                break;
+            case 'ΠρΡβέ΢ης':
+                $regno = '4000115';
+                break;
+            case 'Άρτας':
+                $regno = '0400115';
+                break;
+            case 'Φλωρίνης':
+                $regno = '4700115';
+                break;
+            case 'ΞšΞΏΞΆΞ¬Ξ½Ξ·Ο‚':
+                $regno = '2700105';
+                break;
+            case 'ΞšΞ±ΟƒΟ„ΞΏΟΞΉΞ¬Ο‚':
+                $regno = '2300115';
+                break;
+            case 'Σάμου':
+                $regno = '4300115';
+                break;
+            case 'ΑττικΞ�Ο‚':
+                $regno = $this->getRegistryNoFromTaxCode($taxcode);
+                break;
+            case 'Ξ˜Ξ΅ΟƒΟƒΞ±Ξ»ΞΏΞ½Ξ―ΞΊΞ·Ο‚':
+                $regno = $this->getRegistryNoFromTaxCode($taxcode);
+                break;
+            default:
+                break;
+        }//---switch end -----//
+        if(!empty($regno)) {
+            return $regno;
+        } else 
+           return false;
+    } else {
+        return false;
+    }
+    
+  }
+  /**
+   * Getallschools.
+   *
+   * @return string
+   *   Return Hello string.
+   */
+  public function parseHtmlContent() {
+    //---------------------------------------------------------------------------//
+    //---------------syndesi frontistirion me dieythinsi deyterobatmias----------//
+    //---------------------------------------------------------------------------//
+    //get all existing schools and read their county and postal information
+/*
+   $sCon = $this->connection
+          ->select('licenses_entity','eSchool')
+          ->fields('eSchool', array('id','certCode','jsonDetails'));
+   $counter = 0;
+   $allSchools = $sCon->execute()->fetchAll(\PDO::FETCH_OBJ);
+   
+   if($allSchools) {
+        $list = array();
+        foreach ($allSchools as $object) {
+            $jsonObject = json_decode($object->jsonDetails);
+            if(is_array($jsonObject)) {
+                $parartimata = count($jsonObject);
+                $arrayParartimataObjects = array();
+                    for ($i=0; $i < $parartimata ; $i++) {
+                        $parartimaObj = $jsonObject[$i];
+                        $nomos = $parartimaObj->County;
+                        $taxcode = $parartimaObj->PostalCode;
+                        $buildingId = $jsonObject[$i]->BuildingID;
+                        $registryno = $this->getRegistryNoFromNomos($nomos, $taxcode);
+                        $adminId = $this->getAdminIdFromRegistryNo($registryno); 
+                        if($adminId) {
+                            $metaData = (object) array(
+                                'BuildingID' => $buildingId, 
+                                'DieythinsiID' => $adminId,
+                                'enabled' => 1
+                            );
+                            array_push($arrayParartimataObjects, $metaData);
+                            
+                        } else {
+                            echo $object->certCode.' parartima'.$i.'<br>';
+                        }
+                    }// -- for parartimata ------------//
+                    //update jsonExtra field
+                    if(!empty($arrayParartimataObjects) && count($arrayParartimataObjects)>=1 ) {
+                        $this->connection->update('licenses_entity')
+                        ->fields([
+                          'jsonExtra' => json_encode($arrayParartimataObjects),
+                        ])
+                        ->condition('certCode', $object->certCode, '=')
+                        ->execute();
+                       // echo 'update certcode'.$object->certCode.'<br>';
+                       // break;
+                    } 
+                }//--if inner --//
+            }//--foreach--//
+        }//--if outer --//
+    */
+   
+   //--------------------------------------------------------------------------------//
+   //---------------get all certCodes and create private school users --------------//
+   //--------------------------------------------------------------------------------//
+    $sCon = $this->connection
+    ->select('licenses_entity','eSchool')
+    ->fields('eSchool', array('id','certCode','jsonDetails'));
+
+    $allSchools = $sCon->execute()->fetchAll(\PDO::FETCH_OBJ);
+    //create private school users
+    $certCodes = array();
+    if($allSchools) {
+        foreach ($allSchools as $object) {
+            array_push($certCodes, $object->certCode);
+        }//--foreach--//
+    }//--if outer --//
+    
+    for ($i=0; $i < count($certCodes); $i++) { 
+        // Create user object.
+        $user = User::create();
+    
+        //Mandatory settings
+        $user->setPassword($certCodes[$i]);
+        $user->enforceIsNew();
+        $user->setUsername($certCodes[$i]);
+        $user->addRole('diaxeiristis_frontistiriou'); 
+        $user->save();
+    }
+    exit;
+    //---------------delete private school users----------------------------//
+    //----------------------------------------------------------------------// 
+    /*
+    for ($i=0; $i <count($certCodes) ; $i++) { 
+      $userObj = user_load_by_name($certCodes[$i]); 
+      if($userObj) {
+        user_delete($userObj->id());
+      }
+    }
+    */
+    
+    return array(
+      '#type' => 'markup',
+      '#markup' => $this->t('Hello, World!!!!'.$httpcode.$err),
+    );
+  }
+
+  /**
+   * Getallschools.
+   *
+   * @return string
+   *   Return Hello string.
+   */
+   public function getFrontistiria(ServerRequestInterface $request) {
+
+    if (!$request->getMethod('GET')) {
+      return $this->respondWithStatus([
+          "message" => t("Method Not Allowed")
+      ], Response::HTTP_METHOD_NOT_ALLOWED);
+    }
+
+    $user = $this->currentUser();
+    if (!$user) {
+      return $this->respondWithStatus([
+          'message' => t("User not found"),
+      ], Response::HTTP_FORBIDDEN);
+    }
+    $roles = $user->getRoles();
+    $validRole = false;
+    foreach ($roles as $role) {
+        if ($role === "ministry") {
+            $validRole = true;
+            break;
+        }
+    }
+
+    if (!$validRole) {
+      return $this->respondWithStatus([
+          'message' => t("User Invalid Role"),
+      ], Response::HTTP_FORBIDDEN);
+    }
+    
+    //return the first 20 private schools
+    $query = $this->queryFactory;
+    $licences = $query->get('licenses_entity')->pager(20);
+    $lisIds = $licences->execute(); 
+
+    //$licences = $this->queryFactory->entityQuery('licenses_entity');
+    if($lisIds) {
+        $licences = $this->entityTypeManager->getStorage('licenses_entity')->loadMultiple($lisIds);
+        
+       if ($licences) {
+           $list = array();
+   
+           foreach ($licences as $object) {
+               $list[] = array(
+                               'id' => $object->id(),
+                               'certCode' => $object->getCertCode(),
+                               'jsonDetails' => $object->getJsonDetails()
+                               );
+   
+           }
+   
+           return $this->respondWithStatus($list, Response::HTTP_OK);
+       } else {
+       return $this->respondWithStatus([
+                   'message' => t('Schools not found'),
+               ], Response::HTTP_FORBIDDEN);
+       }
+    } else {
+        return $this->respondWithStatus([
+            'message' => t('Schools not found'),
+        ], Response::HTTP_FORBIDDEN);
+    }
+    
+
+
+
+   }
+
+
+     /**
+   * Getallschools.
+   *
+   * @return string
+   *   Return Hello string.
+   */
+   public function getFrontistirio(ServerRequestInterface $request) {
+
+    if (!$request->getMethod('GET')) {
+      return $this->respondWithStatus([
+          "message" => t("Method Not Allowed")
+      ], Response::HTTP_METHOD_NOT_ALLOWED);
+    }
+    $user = $this->currentUser();
+    if (!$user) {
+      return $this->respondWithStatus([
+          'message' => t("User not found"),
+      ], Response::HTTP_FORBIDDEN);
+    }
+    $roles = $user->getRoles();
+    $validRole = false;
+    foreach ($roles as $role) {
+        if ($role === "diaxeiristis_frontistiriou") {
+            $validRole = true;
+            break;
+        }
+    }
+
+    if (!$validRole) {
+      return $this->respondWithStatus([
+          'message' => t("User Invalid Role"),
+      ], Response::HTTP_FORBIDDEN);
+    }
+
+    $schoolCode = $user->getAccountName();
+    
+    // certCode
+    $schoolArr = $this->entityTypeManager->getStorage('licenses_entity')->loadByProperties(array('certCode' => $schoolCode));
+    $schoolObj = reset($schoolArr);
+    if($schoolObj) {
+      $list[] = array(
+        'id' => $schoolObj->id(),
+        'certCode' => $schoolObj->getCertCode(),
+        'jsonDetails' => $schoolObj->getJsonDetails()
+        );
+        return $this->respondWithStatus($list, Response::HTTP_OK);
+    } else {
+      return $this->respondWithStatus(['message' => t('School not found'),], Response::HTTP_FORBIDDEN);
+    }
+
+   }
+
+
+        /**
+   * Getallschools.
+   *
+   * @return string
+   *   Return Hello string.
+   */
+   public function getFrontistiriaByArea(ServerRequestInterface $request) {
+    
+        if (!$request->getMethod('GET')) {
+          return $this->respondWithStatus([
+              "message" => t("Method Not Allowed")
+          ], Response::HTTP_METHOD_NOT_ALLOWED);
+        }
+        $user = $this->currentUser();
+        if (!$user) {
+          return $this->respondWithStatus([
+              'message' => t("User not found"),
+          ], Response::HTTP_FORBIDDEN);
+        }
+        $roles = $user->getRoles();
+        $validRole = false;
+        foreach ($roles as $role) {
+            if ($role === "area_admin") {
+                $validRole = true;
+                break;
+            }
+        }
+        if (!$validRole) {
+          return $this->respondWithStatus([
+              'message' => t("User Invalid Role"),
+          ], Response::HTTP_FORBIDDEN);
+        }
+    
+        $areaCode = $user->getAccountName();
+        $areaArr = $this->entityTypeManager->getStorage('admin_area_entity')->loadByProperties(array('registryno' => $areaCode));
+        $areaObj = reset($areaArr);
+        $list = array();
+        if($areaObj) {
+          $adminId = $areaObj->id();
+          $options = array();
+          $sCon = $this->connection->query('SELECT * FROM licenses_entity WHERE json_contains(jsonExtra->\'$[*].DieythinsiID\', json_array(\''.$adminId.'\'))');
+          $result = $sCon->fetchAll(\PDO::FETCH_OBJ);
+          if($result) {
+            foreach ($result as $keyObj) {
+                $list[] = array(
+                  'id' => $keyObj->id,
+                  'certCode' => $keyObj->certCode,
+                  'jsonDetails' => $keyObj->jsonDetails
+                );
+            }
+          }
+
+          return $this->respondWithStatus($list, Response::HTTP_OK);
+        } else {
+          return $this->respondWithStatus(['message' => t("Area Code Not Found"),], Response::HTTP_FORBIDDEN);
+        }
+
+       
+    
+  }
+
+  
+    private function respondWithStatus($arr, $s)
+    {
+        $res = new JsonResponse($arr);
+        $res->setStatusCode($s);
+
+        return $res;
+    }
+
+}
diff --git a/drupal/modules/custom/datachanger/templates/datachanger.html.twig b/drupal/modules/custom/datachanger/templates/datachanger.html.twig
new file mode 100755
index 0000000..91e43c8
--- /dev/null
+++ b/drupal/modules/custom/datachanger/templates/datachanger.html.twig
@@ -0,0 +1 @@
+<!-- Add you custom twig html here -->
\ No newline at end of file
diff --git a/drupal/modules/jsonapi/LICENSE.txt b/drupal/modules/jsonapi/LICENSE.txt
new file mode 100644
index 0000000..d159169
--- /dev/null
+++ b/drupal/modules/jsonapi/LICENSE.txt
@@ -0,0 +1,339 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/drupal/modules/jsonapi/README.md b/drupal/modules/jsonapi/README.md
new file mode 100644
index 0000000..b31b74d
--- /dev/null
+++ b/drupal/modules/jsonapi/README.md
@@ -0,0 +1,46 @@
+# JSON API
+The jsonapi module exposes a [JSON API](http://jsonapi.org/) implementation for data stored in Drupal.
+
+The JSON API specification supports [_extensions_](http://jsonapi.org/extensions/). The following extensions are
+supported in this JSON API implementation:
+
+1. [Partial Success](https://gist.github.com/e0ipso/732712c3e573a6af1d83b25b9f0269c8), for when a resource collection is retrieved and only a subset of resources is accessible.
+2. [Fancy Filters](https://gist.github.com/e0ipso/efcc4e96ca2aed58e32948e4f70c2460), to specify the filter strategy exposed by this module.
+
+## Installation
+
+Install the module as every other module.
+
+## Compatibility
+
+This module is compatible with Drupal core 8.2.x and higher.
+
+## Configuration
+
+Unlike the core REST module JSON API doesn't really require any kind of configuration by default.
+
+## Usage
+
+The jsonapi module exposes both config and content entity resources. On top of that it exposes one resource per bundle per entity. The default format appears like: `/jsonapi/{entity_type}/{bundle}/{uuid}?_format=api_json`
+
+The list of endpoints then looks like the following:
+* `/jsonapi/node/article`: Exposes a collection of article content
+* `/jsonapi/node/article/{UUID}`: Exposes an individual article
+* `/jsonapi/block`: Exposes a collection of blocks
+* `/jsonapi/block/{block}`: Exposes an individual block
+
+## Development usage
+
+It is also possible to obtain the JSON API representation of a supported entity:
+
+  ```
+  // For a given $entity object.
+  $nested_array = \Drupal::service('jsonapi.entity.to_jsonapi')->normalize($entity);
+  ```
+
+Should it be needed, the raw string itself can be obtained:
+
+  ```
+  // For a given $entity object.
+  $json_string = \Drupal::service('jsonapi.entity.to_jsonapi')->serialize($entity);
+  ```
diff --git a/drupal/modules/jsonapi/composer.json b/drupal/modules/jsonapi/composer.json
new file mode 100644
index 0000000..e54e7e5
--- /dev/null
+++ b/drupal/modules/jsonapi/composer.json
@@ -0,0 +1,9 @@
+{
+    "name": "drupal/jsonapi",
+    "description": "Provides a JSON API standards-compliant API for accessing and manipulating Drupal content and configuration entities.",
+    "type": "drupal-module",
+    "license": "GPL-2.0+",
+    "require-dev": {
+        "justinrainbow/json-schema": "^4.1"
+    }
+}
diff --git a/drupal/modules/jsonapi/jsonapi.info.yml b/drupal/modules/jsonapi/jsonapi.info.yml
new file mode 100644
index 0000000..6984edb
--- /dev/null
+++ b/drupal/modules/jsonapi/jsonapi.info.yml
@@ -0,0 +1,14 @@
+name: JSON API
+type: module
+description: Provides a JSON API standards-compliant API for accessing and manipulating Drupal content and configuration entities.
+# core: 8.x
+package: Web services
+dependencies:
+  - drupal:system (>=8.2)
+  - serialization
+
+# Information added by Drupal.org packaging script on 2017-07-09
+version: '8.x-1.1'
+core: '8.x'
+project: 'jsonapi'
+datestamp: 1499586846
diff --git a/drupal/modules/jsonapi/jsonapi.module b/drupal/modules/jsonapi/jsonapi.module
new file mode 100644
index 0000000..f304d4e
--- /dev/null
+++ b/drupal/modules/jsonapi/jsonapi.module
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * @file
+ * Module implementation file.
+ */
+
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+
+/**
+ * Implements hook_help().
+ */
+function jsonapi_help($route_name, RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    case 'help.page.jsonapi':
+      $output = '<h3>' . t('About') . '</h3>';
+      $output .= '<p>' . t('The JSON API module is a fully compliant implementation of the <a href=":spec">JSON API Specification</a>. By following shared conventions, you can increase productivity, take advantage of generalized tooling, and focus on what matters: your application. Clients built around JSON API are able to take advantage of its features such as efficiently caching responses, sometimes eliminating network requests entirely. For more information, see the <a href=":docs">online documentation for the JSON API module</a>.', [
+        ':spec' => 'http://jsonapi.org',
+        ':docs' => 'https://www.youtube.com/playlist?list=PLZOQ_ZMpYrZsyO-3IstImK1okrpfAjuMZ',
+      ]) . '</p>';
+      $output .= '<dl>';
+      $output .= '<dt>' . t('General') . '</dt>';
+      $output .= '<dd>' . t('JSON API is a particular implementation of REST that provides conventions for resource relationships, collections, filters, pagination, and sorting, in addition to error handling and full test coverage. These conventions help developers build clients faster and encourages reuse of code.') . '</dd>';
+      $output .= '</dl>';
+
+      return $output;
+  }
+  return NULL;
+}
+
+/**
+ * Implements hook_entity_base_field_info().
+ *
+ * @todo This should probably live in core, but for now we will keep it as a
+ * temporary solution. There are similar unresolved efforts already happening
+ * there.
+ *
+ * @see https://www.drupal.org/node/2825487
+ */
+function jsonapi_entity_base_field_info(EntityTypeInterface $entity_type) {
+  $fields = [];
+  if ($entity_type->id() == 'file') {
+    $fields['url'] = BaseFieldDefinition::create('text')
+      ->setLabel(t('Download URL'))
+      ->setDescription(t('The download URL of the file.'))
+      ->setComputed(TRUE)
+      ->setQueryable(FALSE)
+      ->setClass('\Drupal\jsonapi\Field\FileDownloadUrl')
+      ->setDisplayOptions('view', [
+        'label' => 'above',
+        'weight' => -5,
+      ]);
+  }
+  return $fields;
+}
diff --git a/drupal/modules/jsonapi/jsonapi.permissions.yml b/drupal/modules/jsonapi/jsonapi.permissions.yml
new file mode 100644
index 0000000..f5e5908
--- /dev/null
+++ b/drupal/modules/jsonapi/jsonapi.permissions.yml
@@ -0,0 +1,3 @@
+access jsonapi resource list:
+  description: 'Gives access to the list of resources served by the JSON API module.'
+  title: 'Access JSON API resource list'
diff --git a/drupal/modules/jsonapi/jsonapi.routing.yml b/drupal/modules/jsonapi/jsonapi.routing.yml
new file mode 100644
index 0000000..f4d060c
--- /dev/null
+++ b/drupal/modules/jsonapi/jsonapi.routing.yml
@@ -0,0 +1,3 @@
+route_callbacks:
+  - '\Drupal\jsonapi\Routing\Routes::entryPoint'
+  - '\Drupal\jsonapi\Routing\Routes::routes'
diff --git a/drupal/modules/jsonapi/jsonapi.services.yml b/drupal/modules/jsonapi/jsonapi.services.yml
new file mode 100644
index 0000000..970157e
--- /dev/null
+++ b/drupal/modules/jsonapi/jsonapi.services.yml
@@ -0,0 +1,122 @@
+services:
+  serializer.normalizer.htt_exception.jsonapi:
+    class: Drupal\jsonapi\Normalizer\HttpExceptionNormalizer
+    arguments: ['@current_user']
+    tags:
+      - { name: normalizer, priority: 1 }
+  serializer.normalizer.unprocessable_entity_exception.jsonapi:
+    class: Drupal\jsonapi\Normalizer\UnprocessableHttpEntityExceptionNormalizer
+    arguments: ['@current_user']
+    tags:
+      - { name: normalizer, priority: 2 }
+  serializer.normalizer.entity_access_exception.jsonapi:
+    class: Drupal\jsonapi\Normalizer\EntityAccessDeniedHttpExceptionNormalizer
+    arguments: ['@current_user']
+    tags:
+      - { name: normalizer, priority: 2 }
+  serializer.normalizer.scalar.jsonapi:
+    class: Drupal\jsonapi\Normalizer\ScalarNormalizer
+    tags:
+      - { name: normalizer, priority: 5 }
+  serializer.normalizer.entity_reference_item.jsonapi:
+    class: Drupal\jsonapi\Normalizer\RelationshipItemNormalizer
+    arguments: ['@jsonapi.resource_type.repository', '@serializer.normalizer.jsonapi_document_toplevel.jsonapi',]
+    tags:
+      - { name: normalizer, priority: 21 }
+  serializer.normalizer.field_item.jsonapi:
+    class: Drupal\jsonapi\Normalizer\FieldItemNormalizer
+    tags:
+      - { name: normalizer, priority: 21 }
+  serializer.normalizer.field.jsonapi:
+    class: Drupal\jsonapi\Normalizer\FieldNormalizer
+    tags:
+      - { name: normalizer, priority: 21 }
+  serializer.normalizer.relationship.jsonapi:
+    class: Drupal\jsonapi\Normalizer\RelationshipNormalizer
+    arguments: ['@jsonapi.resource_type.repository', '@jsonapi.link_manager']
+    tags:
+      - { name: normalizer, priority: 21 }
+  serializer.normalizer.entity.jsonapi:
+    class: Drupal\jsonapi\Normalizer\ContentEntityNormalizer
+    arguments: ['@jsonapi.link_manager', '@jsonapi.resource_type.repository', '@entity_type.manager']
+    tags:
+      - { name: normalizer, priority: 21 }
+  serializer.normalizer.config_entity.jsonapi:
+    class: Drupal\jsonapi\Normalizer\ConfigEntityNormalizer
+    arguments: ['@jsonapi.link_manager', '@jsonapi.resource_type.repository', '@entity_type.manager']
+    tags:
+      - { name: normalizer, priority: 21 }
+  serializer.normalizer.jsonapi_document_toplevel.jsonapi:
+    class: Drupal\jsonapi\Normalizer\JsonApiDocumentTopLevelNormalizer
+    arguments: ['@jsonapi.link_manager', '@jsonapi.current_context', '@entity_type.manager', '@jsonapi.resource_type.repository']
+    tags:
+      - { name: normalizer, priority: 22 }
+  serializer.normalizer.entity_reference_field.jsonapi:
+    class: Drupal\jsonapi\Normalizer\EntityReferenceFieldNormalizer
+    arguments: ['@jsonapi.link_manager', '@entity_field.manager', '@plugin.manager.field.field_type', '@jsonapi.resource_type.repository', '@entity.repository']
+    tags:
+      - { name: normalizer, priority: 31 }
+  serializer.encoder.jsonapi:
+    class: Drupal\jsonapi\Encoder\JsonEncoder
+    tags:
+      - { name: encoder, priority: 21, format: 'api_json' }
+  jsonapi.resource_type.repository:
+    class: Drupal\jsonapi\ResourceType\ResourceTypeRepository
+    arguments: ['@entity_type.manager', '@entity_type.bundle.info']
+  jsonapi.route_enhancer:
+    class: Drupal\jsonapi\Routing\RouteEnhancer
+    tags:
+      - { name: route_enhancer }
+  jsonapi.params.enhancer:
+    class: Drupal\jsonapi\Routing\JsonApiParamEnhancer
+    arguments: ['@entity_field.manager']
+    tags:
+      - { name: route_enhancer }
+  jsonapi.query_builder:
+    class: Drupal\jsonapi\Query\QueryBuilder
+    arguments: ['@entity_type.manager', '@jsonapi.current_context', '@jsonapi.field_resolver']
+  jsonapi.link_manager:
+    class: Drupal\jsonapi\LinkManager\LinkManager
+    arguments: ['@router.no_access_checks', '@url_generator']
+  jsonapi.current_context:
+    class: Drupal\jsonapi\Context\CurrentContext
+    arguments: ['@jsonapi.resource_type.repository', '@request_stack', '@current_route_match']
+  jsonapi.field_resolver:
+    class: Drupal\jsonapi\Context\FieldResolver
+    arguments: ['@jsonapi.current_context', '@entity_field.manager', '@entity_type.bundle.info', '@jsonapi.resource_type.repository']
+  access_check.jsonapi.custom_query_parameter_names:
+    class: Drupal\jsonapi\Access\CustomQueryParameterNamesAccessCheck
+    tags:
+      - { name: access_check, applies_to: _jsonapi_custom_query_parameter_names, needs_incoming_request: TRUE }
+  paramconverter.jsonapi.entity_uuid:
+    class: Drupal\jsonapi\ParamConverter\EntityUuidConverter
+    tags:
+      # Priority 10, to ensure it runs before @paramconverter.entity.
+      - { name: paramconverter, priority: 10 }
+    arguments: ['@entity.manager']
+  jsonapi.exception_subscriber:
+    class: Drupal\jsonapi\EventSubscriber\DefaultExceptionSubscriber
+    tags:
+      - { name: event_subscriber }
+    arguments: ['@serializer', '%serializer.formats%']
+  jsonapi.http_middleware.format_setter:
+    class: Drupal\jsonapi\StackMiddleware\FormatSetter
+    tags:
+      # Set priority to 201 so it happens right before the page cache
+      # middleware (priority 200)has the opportunity to respond.
+      - { name: http_middleware, priority: 201 }
+
+  jsonapi.entity.to_jsonapi:
+    class: Drupal\jsonapi\EntityToJsonApi
+    arguments: ['@serializer', '@jsonapi.resource_type.repository', '@current_user']
+
+  logger.channel.jsonapi:
+    parent: logger.channel_base
+    arguments: ['jsonapi']
+
+  # Event subscribers.
+  jsonapi.resource_response.subscriber:
+    class: Drupal\jsonapi\EventSubscriber\ResourceResponseSubscriber
+    tags:
+      - { name: event_subscriber }
+    arguments: ['@serializer', '@renderer', '@logger.channel.jsonapi']
diff --git a/drupal/modules/jsonapi/phpcs.xml b/drupal/modules/jsonapi/phpcs.xml
new file mode 100644
index 0000000..686fca8
--- /dev/null
+++ b/drupal/modules/jsonapi/phpcs.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ruleset name="jsonapi">
+  <description>Default PHP CodeSniffer configuration for RESTful.</description>
+  <file>.</file>
+  <arg name="extensions" value="inc,install,module,php,profile,test,theme,yml"/>
+
+  <!--Blacklist of coding standard rules that are not yet fixed. -->
+  <rule ref="Drupal">
+    <exclude name="Drupal.Commenting.DocComment.MissingShort"/>
+    <exclude name="Drupal.Commenting.FunctionComment.IncorrectTypeHint"/>
+    <exclude name="Drupal.Commenting.FunctionComment.MissingReturnComment"/>
+    <exclude name="Drupal.NamingConventions.ValidVariableName.LowerCamelName"/>
+  </rule>
+</ruleset>
diff --git a/drupal/modules/jsonapi/schema.json b/drupal/modules/jsonapi/schema.json
new file mode 100644
index 0000000..902a39d
--- /dev/null
+++ b/drupal/modules/jsonapi/schema.json
@@ -0,0 +1,375 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "JSON API Schema",
+  "description": "This is a schema for responses in the JSON API format. For more, see http://jsonapi.org",
+  "oneOf": [
+    {
+      "$ref": "#/definitions/success"
+    },
+    {
+      "$ref": "#/definitions/failure"
+    },
+    {
+      "$ref": "#/definitions/info"
+    }
+  ],
+
+  "definitions": {
+    "success": {
+      "type": "object",
+      "required": [
+        "data"
+      ],
+      "properties": {
+        "data": {
+          "$ref": "#/definitions/data"
+        },
+        "included": {
+          "description": "To reduce the number of HTTP requests, servers **MAY** allow responses that include related resources along with the requested primary resources. Such responses are called \"compound documents\".",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/resource"
+          },
+          "uniqueItems": true
+        },
+        "meta": {
+          "$ref": "#/definitions/meta"
+        },
+        "links": {
+          "description": "Link members related to the primary data.",
+          "allOf": [
+            {
+              "$ref": "#/definitions/links"
+            },
+            {
+              "$ref": "#/definitions/pagination"
+            }
+          ]
+        },
+        "jsonapi": {
+          "$ref": "#/definitions/jsonapi"
+        }
+      },
+      "additionalProperties": false
+    },
+    "failure": {
+      "type": "object",
+      "required": [
+        "errors"
+      ],
+      "properties": {
+        "errors": {
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/error"
+          },
+          "uniqueItems": true
+        },
+        "meta": {
+          "$ref": "#/definitions/meta"
+        },
+        "jsonapi": {
+          "$ref": "#/definitions/jsonapi"
+        }
+      },
+      "additionalProperties": false
+    },
+    "info": {
+      "type": "object",
+      "required": [
+        "meta"
+      ],
+      "properties": {
+        "meta": {
+          "$ref": "#/definitions/meta"
+        },
+        "links": {
+          "$ref": "#/definitions/links"
+        },
+        "jsonapi": {
+          "$ref": "#/definitions/jsonapi"
+        }
+      },
+      "additionalProperties": false
+    },
+
+    "meta": {
+      "description": "Non-standard meta-information that can not be represented as an attribute or relationship.",
+      "type": "object",
+      "additionalProperties": true
+    },
+    "data": {
+      "description": "The document's \"primary data\" is a representation of the resource or collection of resources targeted by a request.",
+      "oneOf": [
+        {
+          "$ref": "#/definitions/resource"
+        },
+        {
+          "description": "An array of resource objects, an array of resource identifier objects, or an empty array ([]), for requests that target resource collections.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/resource"
+          },
+          "uniqueItems": true
+        },
+        {
+          "description": "null if the request is one that might correspond to a single resource, but doesn't currently.",
+          "type": "null"
+        }
+      ]
+    },
+    "resource": {
+      "description": "\"Resource objects\" appear in a JSON API document to represent resources.",
+      "type": "object",
+      "required": [
+        "type",
+        "id"
+      ],
+      "properties": {
+        "type": {
+          "type": "string"
+        },
+        "id": {
+          "type": "string"
+        },
+        "attributes": {
+          "$ref": "#/definitions/attributes"
+        },
+        "relationships": {
+          "$ref": "#/definitions/relationships"
+        },
+        "links": {
+          "$ref": "#/definitions/links"
+        },
+        "meta": {
+          "$ref": "#/definitions/meta"
+        }
+      },
+      "additionalProperties": false
+    },
+
+    "links": {
+      "description": "A resource object **MAY** contain references to other resource objects (\"relationships\"). Relationships may be to-one or to-many. Relationships can be specified by including a member in a resource's links object.",
+      "type": "object",
+      "properties": {
+        "self": {
+          "description": "A `self` member, whose value is a URL for the relationship itself (a \"relationship URL\"). This URL allows the client to directly manipulate the relationship. For example, it would allow a client to remove an `author` from an `article` without deleting the people resource itself.",
+          "type": "string",
+          "format": "uri"
+        },
+        "related": {
+          "$ref": "#/definitions/link"
+        }
+      },
+      "additionalProperties": true
+    },
+    "link": {
+      "description": "A link **MUST** be represented as either: a string containing the link's URL or a link object.",
+      "oneOf": [
+        {
+          "description": "A string containing the link's URL.",
+          "type": "string",
+          "format": "uri"
+        },
+        {
+          "type": "object",
+          "required": [
+            "href"
+          ],
+          "properties": {
+            "href": {
+              "description": "A string containing the link's URL.",
+              "type": "string",
+              "format": "uri"
+            },
+            "meta": {
+              "$ref": "#/definitions/meta"
+            }
+          }
+        }
+      ]
+    },
+
+    "attributes": {
+      "description": "Members of the attributes object (\"attributes\") represent information about the resource object in which it's defined.",
+      "type": "object",
+      "patternProperties": {
+        "^(?!relationships$|links$)\\w[-\\w_]*$": {
+          "description": "Attributes may contain any valid JSON value."
+        }
+      },
+      "additionalProperties": false
+    },
+
+    "relationships": {
+      "description": "Members of the relationships object (\"relationships\") represent references from the resource object in which it's defined to other resource objects.",
+      "type": "object",
+      "patternProperties": {
+        "^\\w[-\\w_]*$": {
+          "properties": {
+            "links": {
+              "$ref": "#/definitions/links"
+            },
+            "data": {
+              "description": "Member, whose value represents \"resource linkage\".",
+              "oneOf": [
+                {
+                  "$ref": "#/definitions/relationshipToOne"
+                },
+                {
+                  "$ref": "#/definitions/relationshipToMany"
+                }
+              ]
+            },
+            "meta": {
+              "$ref": "#/definitions/meta"
+            }
+          },
+          "anyOf": [
+            {"required": ["data"]},
+            {"required": ["meta"]},
+            {"required": ["links"]}
+          ],
+          "additionalProperties": false
+        }
+      },
+      "additionalProperties": false
+    },
+    "relationshipToOne": {
+      "description": "References to other resource objects in a to-one (\"relationship\"). Relationships can be specified by including a member in a resource's links object.",
+      "anyOf": [
+        {
+          "$ref": "#/definitions/empty"
+        },
+        {
+          "$ref": "#/definitions/linkage"
+        }
+      ]
+    },
+    "relationshipToMany": {
+      "description": "An array of objects each containing \"type\" and \"id\" members for to-many relationships.",
+      "type": "array",
+      "items": {
+        "$ref": "#/definitions/linkage"
+      },
+      "uniqueItems": true
+    },
+    "empty": {
+      "description": "Describes an empty to-one relationship.",
+      "type": "null"
+    },
+    "linkage": {
+      "description": "The \"type\" and \"id\" to non-empty members.",
+      "type": "object",
+      "required": [
+        "type",
+        "id"
+      ],
+      "properties": {
+        "type": {
+          "type": "string"
+        },
+        "id": {
+          "type": "string"
+        },
+        "meta": {
+          "$ref": "#/definitions/meta"
+        }
+      },
+      "additionalProperties": false
+    },
+    "pagination": {
+      "type": "object",
+      "properties": {
+        "first": {
+          "description": "The first page of data",
+          "oneOf": [
+            { "type": "string", "format": "uri" },
+            { "type": "null" }
+          ]
+        },
+        "last": {
+          "description": "The last page of data",
+          "oneOf": [
+            { "type": "string", "format": "uri" },
+            { "type": "null" }
+          ]
+        },
+        "prev": {
+          "description": "The previous page of data",
+          "oneOf": [
+            { "type": "string", "format": "uri" },
+            { "type": "null" }
+          ]
+        },
+        "next": {
+          "description": "The next page of data",
+          "oneOf": [
+            { "type": "string", "format": "uri" },
+            { "type": "null" }
+          ]
+        }
+      }
+    },
+
+    "jsonapi": {
+      "description": "An object describing the server's implementation",
+      "type": "object",
+      "properties": {
+        "version": {
+          "type": "string"
+        },
+        "meta": {
+          "$ref": "#/definitions/meta"
+        }
+      },
+      "additionalProperties": false
+    },
+
+    "error": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "description": "A unique identifier for this particular occurrence of the problem.",
+          "type": "string"
+        },
+        "links": {
+          "$ref": "#/definitions/links"
+        },
+        "status": {
+          "description": "The HTTP status code applicable to this problem, expressed as a string value.",
+          "type": "string"
+        },
+        "code": {
+          "description": "An application-specific error code, expressed as a string value.",
+          "type": "string"
+        },
+        "title": {
+          "description": "A short, human-readable summary of the problem. It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization.",
+          "type": "string"
+        },
+        "detail": {
+          "description": "A human-readable explanation specific to this occurrence of the problem.",
+          "type": "string"
+        },
+        "source": {
+          "type": "object",
+          "properties": {
+            "pointer": {
+              "description": "A JSON Pointer [RFC6901] to the associated entity in the request document [e.g. \"/data\" for a primary data object, or \"/data/attributes/title\" for a specific attribute].",
+              "type": "string"
+            },
+            "parameter": {
+              "description": "A string indicating which query parameter caused the error.",
+              "type": "string"
+            }
+          }
+        },
+        "meta": {
+          "$ref": "#/definitions/meta"
+        }
+      },
+      "additionalProperties": false
+    }
+  }
+}
diff --git a/drupal/modules/jsonapi/src/Access/CustomQueryParameterNamesAccessCheck.php b/drupal/modules/jsonapi/src/Access/CustomQueryParameterNamesAccessCheck.php
new file mode 100644
index 0000000..659115d
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Access/CustomQueryParameterNamesAccessCheck.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Drupal\jsonapi\Access;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Routing\Access\AccessInterface;
+use Drupal\jsonapi\JsonApiSpec;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Validates custom (implementation-specific) query parameter names.
+ *
+ * @see http://jsonapi.org/format/#query-parameters
+ *
+ * @internal
+ */
+class CustomQueryParameterNamesAccessCheck implements AccessInterface {
+
+  /**
+   * Denies access when using invalid custom JSON API query parameter names.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request.
+   *
+   * @return \Drupal\Core\Access\AccessResult
+   *   The access result.
+   */
+  public function access(Request $request) {
+    $json_api_params = $request->attributes->get('_json_api_params', []);
+    if (!$this->validate($json_api_params)) {
+      return AccessResult::forbidden();
+    }
+    return AccessResult::allowed();
+  }
+
+  /**
+   * Validates custom JSON API query parameters.
+   *
+   * @param string[] $json_api_params
+   *   The JSON API parameters.
+   *
+   * @return bool
+   */
+  protected function validate(array $json_api_params) {
+    foreach (array_keys($json_api_params) as $query_parameter_name) {
+      // Ignore reserved (official) query parameters.
+      if (in_array($query_parameter_name, JsonApiSpec::getReservedQueryParameters())) {
+        continue;
+      }
+
+      if (!JsonApiSpec::isValidCustomQueryParameter($query_parameter_name)) {
+        return FALSE;
+      }
+    }
+
+    return TRUE;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Context/CurrentContext.php b/drupal/modules/jsonapi/src/Context/CurrentContext.php
new file mode 100644
index 0000000..05fa7d2
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Context/CurrentContext.php
@@ -0,0 +1,150 @@
+<?php
+
+namespace Drupal\jsonapi\Context;
+
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\jsonapi\ResourceType\ResourceTypeRepository;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * Service for accessing information about the current JSON API request.
+ *
+ * @internal
+ */
+class CurrentContext {
+
+  /**
+   * The JSON API resource type repository.
+   *
+   * @var \Drupal\jsonapi\ResourceType\ResourceTypeRepository
+   */
+  protected $resourceTypeRepository;
+
+  /**
+   * The current JSON API resource type.
+   *
+   * @var \Drupal\jsonapi\ResourceType\ResourceType
+   */
+  protected $resourceType;
+
+  /**
+   * The current request.
+   *
+   * @var \Symfony\Component\HttpFoundation\RequestStack
+   */
+  protected $requestStack;
+
+  /**
+   * The current route match.
+   *
+   * @var \Drupal\Core\Routing\RouteMatchInterface
+   */
+  protected $routeMatch;
+
+  /**
+   * Creates a CurrentContext object.
+   *
+   * @param \Drupal\jsonapi\ResourceType\ResourceTypeRepository $resource_type_repository
+   *   The resource type repository.
+   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
+   *   The request stack.
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The current route match.
+   */
+  public function __construct(ResourceTypeRepository $resource_type_repository, RequestStack $request_stack, RouteMatchInterface $route_match) {
+    $this->resourceTypeRepository = $resource_type_repository;
+    $this->requestStack = $request_stack;
+    $this->routeMatch = $route_match;
+  }
+
+  /**
+   * Gets the JSON API resource type for the current request.
+   *
+   * @return \Drupal\jsonapi\ResourceType\ResourceType
+   *   The JSON API resource type for the current request.
+   */
+  public function getResourceType() {
+    if (!isset($this->resourceType)) {
+      $route = $this->routeMatch->getRouteObject();
+      $entity_type_id = $route->getRequirement('_entity_type');
+      $bundle = $route->getRequirement('_bundle');
+      $this->resourceType = $this->resourceTypeRepository
+        ->get($entity_type_id, $bundle);
+    }
+
+    return $this->resourceType;
+  }
+
+  /**
+   * Checks if the request is on a relationship.
+   *
+   * @return bool
+   *   TRUE if the request is on a relationship. FALSE otherwise.
+   */
+  public function isOnRelationship() {
+    return (bool) $this->routeMatch
+      ->getRouteObject()
+      ->getDefault('_on_relationship');
+  }
+
+  /**
+   * Get a value by key from the _json_api_params route parameter.
+   *
+   * @param string $parameter_key
+   *   The key by which to retrieve a route parameter.
+   *
+   * @return mixed
+   *   The JSON API provided parameter.
+   */
+  public function getJsonApiParameter($parameter_key) {
+    $params = $this
+      ->requestStack
+      ->getCurrentRequest()
+      ->attributes
+      ->get('_json_api_params');
+
+    return isset($params[$parameter_key]) ? $params[$parameter_key] : NULL;
+  }
+
+  /**
+   * Determines, whether the JSONAPI extension was requested.
+   *
+   * @todo Find a better place for such a JSONAPI derived information.
+   *
+   * @param string $extension_name
+   *   The extension name.
+   *
+   * @return bool
+   *   Returns TRUE, if the extension has been found.
+   */
+  public function hasExtension($extension_name) {
+    return in_array($extension_name, $this->getExtensions());
+  }
+
+  /**
+   * Returns a list of requested extensions.
+   *
+   * @return string[]
+   *   The extension names.
+   */
+  public function getExtensions() {
+    $content_type_header = $this
+      ->requestStack
+      ->getCurrentRequest()
+      ->headers
+      ->get('Content-Type');
+    if (preg_match('/ext="([^"]+)"/i', $content_type_header, $match)) {
+      $extensions = array_map('trim', explode(',', $match[1]));
+      return $extensions;
+    }
+    return [];
+  }
+
+  /**
+   * Reset the internal caches.
+   */
+  public function reset() {
+    unset($this->resourceType);
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Context/FieldResolver.php b/drupal/modules/jsonapi/src/Context/FieldResolver.php
new file mode 100644
index 0000000..9449f15
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Context/FieldResolver.php
@@ -0,0 +1,188 @@
+<?php
+
+namespace Drupal\jsonapi\Context;
+
+use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\TypedData\DataReferenceTargetDefinition;
+use Drupal\jsonapi\ResourceType\ResourceType;
+use Drupal\jsonapi\ResourceType\ResourceTypeRepository;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+
+/**
+ * Service which resolves public field names to and from Drupal field names.
+ *
+ * @internal
+ */
+class FieldResolver {
+
+  /**
+   * The entity type id.
+   *
+   * @var \Drupal\jsonapi\Context\CurrentContext
+   */
+  protected $currentContext;
+
+  /**
+   * The field manager.
+   *
+   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
+   */
+  protected $fieldManager;
+
+  /**
+   * Creates a FieldResolver instance.
+   *
+   * @param \Drupal\jsonapi\Context\CurrentContext $current_context
+   *   The JSON API CurrentContext service.
+   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $field_manager
+   *   The field manager.
+   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
+   *   The bundle info service.
+   * @param \Drupal\jsonapi\ResourceType\ResourceTypeRepository $resource_type_repository
+   *   The resource type repository.
+   */
+  public function __construct(CurrentContext $current_context, EntityFieldManagerInterface $field_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, ResourceTypeRepository $resource_type_repository) {
+    $this->currentContext = $current_context;
+    $this->fieldManager = $field_manager;
+    $this->entityTypeBundleInfo = $entity_type_bundle_info;
+    $this->resourceTypeRepository = $resource_type_repository;
+  }
+
+  /**
+   * Maps a Drupal field name to a public field name.
+   *
+   * Example:
+   *   'field_author.entity.field_first_name' -> 'author.firstName'.
+   *
+   * @param string $field_name
+   *   The Drupal field name to map to a public field name.
+   *
+   * @return string
+   *   The mapped field name.
+   */
+  public function resolveExternal($internal_field_name) {
+    $resource_type = $this->currentContext->getResourceType();
+    return $resource_type->getPublicName($internal_field_name);
+  }
+
+  /**
+   * Maps a public field name to a Drupal field name.
+   *
+   * Example:
+   *   'author.firstName' -> 'field_author.entity.field_first_name'.
+   *
+   * @param string $field_name
+   *   The public field name to map to a Drupal field name.
+   *
+   * @return string
+   *   The mapped field name.
+   */
+  public function resolveInternal($external_field_name) {
+    $resource_type = $this->currentContext->getResourceType();
+    if (empty($external_field_name)) {
+      throw new BadRequestHttpException('No field name was provided for the filter.');
+    }
+    // Right now we are exposing all the fields with the name they have in
+    // the Drupal backend. But this may change in the future.
+    if (strpos($external_field_name, '.') === FALSE) {
+      return $resource_type->getInternalName($external_field_name);
+    }
+    // Turns 'uid.categories.name' into
+    // 'uid.entity.field_category.entity.name'. This may be too simple, but it
+    // works for the time being.
+    $parts = explode('.', $external_field_name);
+    $entity_type_id = $this->currentContext->getResourceType()->getEntityTypeId();
+    $reference_breadcrumbs = [];
+    $resource_types = [$resource_type];
+    while ($field_name = array_shift($parts)) {
+      $field_name = $this->getInternalName($field_name, $resource_types);
+      if (!$definitions = $this->fieldManager->getFieldStorageDefinitions($entity_type_id)) {
+        throw new BadRequestHttpException(sprintf(
+          'Invalid nested filtering. There is no entity type "%s".',
+          $entity_type_id
+        ));
+      }
+      if (empty($definitions[$field_name])) {
+        throw new BadRequestHttpException(sprintf(
+          'Invalid nested filtering. Invalid entity reference "%s".',
+          $field_name
+        ));
+      }
+      array_push($reference_breadcrumbs, $field_name);
+      // Update the resource type with the referenced type.
+      $resource_types = $this->collectResourceTypesForReference($definitions[$field_name]);
+      // Update the entity type with the referenced type.
+      $entity_type_id = $definitions[$field_name]->getSetting('target_type');
+      // $field_name may not be a reference field. In that case we should treat
+      // the rest of the parts as complex fields.
+      if (empty($entity_type_id)) {
+        // This is the path from the initial entity type to the entity type that
+        // contains $field_name. This path is a set of entity references.
+        $entity_path = implode('.entity.', $reference_breadcrumbs);
+        // This is the path from the final entity type to the selected field
+        // column.
+        $field_path = implode('.', $parts);
+
+        return implode('.', array_filter([$entity_path, $field_path]));
+      }
+    }
+
+    return implode('.entity.', $reference_breadcrumbs);
+  }
+
+  /**
+   * Resolves the internal field name based on a collection of resource types.
+   *
+   * @param string $field_name
+   * @param \Drupal\jsonapi\ResourceType\ResourceType[] $resource_types
+   *
+   * @return string
+   *   The resolved internal name.
+   */
+  protected function getInternalName($field_name, array $resource_types) {
+    return array_reduce($resource_types, function ($carry, ResourceType $resource_type) use ($field_name) {
+      if ($carry != $field_name) {
+        // We already found the internal name.
+        return $carry;
+      }
+      return $resource_type->getInternalName($field_name);
+    }, $field_name);
+  }
+
+  /**
+   * Build a list of resource types depending on which bundles are referenced.
+   *
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   The reference definition.
+   *
+   * @return \Drupal\jsonapi\ResourceType\ResourceType[]
+   *   The list of resource types.
+   */
+  protected function collectResourceTypesForReference(FieldStorageDefinitionInterface $field_definition) {
+    // Check if the field is a flavor of an Entity Reference field.
+    $main_property_definition = $field_definition->getPropertyDefinition(
+      $field_definition->getMainPropertyName()
+    );
+    if (!$main_property_definition instanceof DataReferenceTargetDefinition) {
+      return [];
+    }
+    $entity_type_id = $field_definition->getSetting('target_type');
+    $handler_settings = $field_definition->getSetting('handler_settings');
+    $target_bundles = empty($handler_settings['target_bundles'])
+      ? []
+      : $handler_settings['target_bundles'];
+    if ($target_bundles === NULL) {
+      // If target bundles is NULL it means ALL of the bundles in the entity ID
+      // are referenceable.
+      $bundle_info = $this->entityTypeBundleInfo
+        ->getBundleInfo($entity_type_id);
+      $target_bundles = array_keys($bundle_info);
+    }
+    return array_map(function ($bundle) use ($entity_type_id) {
+      return $this->resourceTypeRepository->get($entity_type_id, $bundle);
+    }, $target_bundles);
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Controller/EntityResource.php b/drupal/modules/jsonapi/src/Controller/EntityResource.php
new file mode 100644
index 0000000..6fbb175
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Controller/EntityResource.php
@@ -0,0 +1,808 @@
+<?php
+
+namespace Drupal\jsonapi\Controller;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Core\Access\AccessibleInterface;
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
+use Drupal\Core\Field\FieldTypePluginManagerInterface;
+use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
+use Drupal\jsonapi\Context\CurrentContext;
+use Drupal\jsonapi\Exception\EntityAccessDeniedHttpException;
+use Drupal\jsonapi\Exception\UnprocessableHttpEntityException;
+use Drupal\jsonapi\LinkManager\LinkManager;
+use Drupal\jsonapi\Query\QueryBuilder;
+use Drupal\jsonapi\Resource\EntityCollection;
+use Drupal\jsonapi\Resource\JsonApiDocumentTopLevel;
+use Drupal\jsonapi\ResourceResponse;
+use Drupal\jsonapi\ResourceType\ResourceType;
+use Drupal\jsonapi\Routing\Param\JsonApiParamBase;
+use Drupal\jsonapi\Routing\Param\OffsetPage;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+
+/**
+ * @see \Drupal\jsonapi\Controller\RequestHandler
+ * @internal
+ */
+class EntityResource {
+
+  /**
+   * The JSON API resource type.
+   *
+   * @var \Drupal\jsonapi\ResourceType\ResourceType
+   */
+  protected $resourceType;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The field manager.
+   *
+   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
+   */
+  protected $fieldManager;
+
+  /**
+   * The query builder service.
+   *
+   * @var \Drupal\jsonapi\Query\QueryBuilder
+   */
+  protected $queryBuilder;
+
+  /**
+   * The current context service.
+   *
+   * @var \Drupal\jsonapi\Context\CurrentContext
+   */
+  protected $currentContext;
+
+  /**
+   * The current context service.
+   *
+   * @var \Drupal\Core\Field\FieldTypePluginManagerInterface
+   */
+  protected $pluginManager;
+
+  /**
+   * The link manager service.
+   *
+   * @var \Drupal\jsonapi\LinkManager\LinkManager
+   */
+  protected $linkManager;
+
+  /**
+   * Instantiates a EntityResource object.
+   *
+   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
+   *   The JSON API resource type.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\jsonapi\Query\QueryBuilder $query_builder
+   *   The query builder.
+   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $field_manager
+   *   The entity type field manager.
+   * @param \Drupal\jsonapi\Context\CurrentContext $current_context
+   *   The current context.
+   * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $plugin_manager
+   *   The plugin manager for fields.
+   * @param \Drupal\jsonapi\LinkManager\LinkManager $link_manager
+   *   The link manager service.
+   */
+  public function __construct(ResourceType $resource_type, EntityTypeManagerInterface $entity_type_manager, QueryBuilder $query_builder, EntityFieldManagerInterface $field_manager, CurrentContext $current_context, FieldTypePluginManagerInterface $plugin_manager, LinkManager $link_manager) {
+    $this->resourceType = $resource_type;
+    $this->entityTypeManager = $entity_type_manager;
+    $this->queryBuilder = $query_builder;
+    $this->fieldManager = $field_manager;
+    $this->currentContext = $current_context;
+    $this->pluginManager = $plugin_manager;
+    $this->linkManager = $link_manager;
+  }
+
+  /**
+   * Gets the individual entity.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The loaded entity.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object.
+   * @param int $response_code
+   *   The response code. Defaults to 200.
+   *
+   * @return \Drupal\jsonapi\ResourceResponse
+   *   The response.
+   */
+  public function getIndividual(EntityInterface $entity, Request $request, $response_code = 200) {
+    $entity_access = $entity->access('view', NULL, TRUE);
+    if (!$entity_access->isAllowed()) {
+      throw new EntityAccessDeniedHttpException($entity, $entity_access, '/data', 'The current user is not allowed to GET the selected resource.');
+    }
+    $response = $this->buildWrappedResponse($entity, $response_code);
+    return $response;
+  }
+
+  /**
+   * Verifies that the whole entity does not violate any validation constraints.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity object.
+   *
+   * @throws \Drupal\jsonapi\Exception\EntityAccessDeniedHttpException
+   *   If validation errors are found.
+   */
+  protected function validate(EntityInterface $entity) {
+    if (!$entity instanceof FieldableEntityInterface) {
+      return;
+    }
+
+    $violations = $entity->validate();
+
+    // Remove violations of inaccessible fields as they cannot stem from our
+    // changes.
+    $violations->filterByFieldAccess();
+
+    if (count($violations) > 0) {
+      // Instead of returning a generic 400 response we use the more specific
+      // 422 Unprocessable Entity code from RFC 4918. That way clients can
+      // distinguish between general syntax errors in bad serializations (code
+      // 400) and semantic errors in well-formed requests (code 422).
+      $exception = new UnprocessableHttpEntityException();
+      $exception->setViolations($violations);
+      throw $exception;
+    }
+  }
+
+  /**
+   * Creates an individual entity.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The loaded entity.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object.
+   *
+   * @return \Drupal\jsonapi\ResourceResponse
+   *   The response.
+   */
+  public function createIndividual(EntityInterface $entity, Request $request) {
+    $entity_access = $entity->access('create', NULL, TRUE);
+
+    if (!$entity_access->isAllowed()) {
+      throw new EntityAccessDeniedHttpException($entity, $entity_access, '/data', 'The current user is not allowed to POST the selected resource.');
+    }
+    $this->validate($entity);
+
+    // Return a 409 Conflict response in accordance with the JSON API spec. See
+    // http://jsonapi.org/format/#crud-creating-responses-409.
+    if ($this->entityExists($entity)) {
+      throw new ConflictHttpException('Conflict: Entity already exists.');
+    }
+
+    $entity->save();
+
+    // Build response object.
+    $response = $this->getIndividual($entity, $request, 201);
+
+    // According to JSON API specification, when a new entity was created
+    // we should send "Location" header to the frontend.
+    $entity_url = $this->linkManager->getEntityLink(
+      $entity->uuid(),
+      $this->resourceType,
+      [],
+      'individual'
+    );
+    $response->headers->set('Location', $entity_url);
+
+    // Return response object with updated headers info.
+    return $response;
+  }
+
+  /**
+   * Patches an individual entity.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The loaded entity.
+   * @param \Drupal\Core\Entity\EntityInterface $parsed_entity
+   *   The entity with the new data.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object.
+   *
+   * @return \Drupal\jsonapi\ResourceResponse
+   *   The response.
+   */
+  public function patchIndividual(EntityInterface $entity, EntityInterface $parsed_entity, Request $request) {
+    $entity_access = $entity->access('update', NULL, TRUE);
+    if (!$entity_access->isAllowed()) {
+      throw new EntityAccessDeniedHttpException($entity, $entity_access, '/data', 'The current user is not allowed to PATCH the selected resource.');
+    }
+    $body = Json::decode($request->getContent());
+    $data = $body['data'];
+    if ($data['id'] != $entity->uuid()) {
+      throw new BadRequestHttpException(sprintf(
+        'The selected entity (%s) does not match the ID in the payload (%s).',
+        $entity->uuid(),
+        $data['id']
+      ));
+    }
+    $data += ['attributes' => [], 'relationships' => []];
+    $field_names = array_merge(array_keys($data['attributes']), array_keys($data['relationships']));
+    array_reduce($field_names, function (EntityInterface $destination, $field_name) use ($parsed_entity) {
+      $this->updateEntityField($parsed_entity, $destination, $field_name);
+      return $destination;
+    }, $entity);
+
+    $this->validate($entity);
+    $entity->save();
+    return $this->getIndividual($entity, $request);
+  }
+
+  /**
+   * Deletes an individual entity.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The loaded entity.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object.
+   *
+   * @return \Drupal\jsonapi\ResourceResponse
+   *   The response.
+   */
+  public function deleteIndividual(EntityInterface $entity, Request $request) {
+    $entity_access = $entity->access('delete', NULL, TRUE);
+    if (!$entity_access->isAllowed()) {
+      throw new EntityAccessDeniedHttpException($entity, $entity_access, '/data', 'The current user is not allowed to DELETE the selected resource.');
+    }
+    $entity->delete();
+    return new ResourceResponse(NULL, 204);
+  }
+
+  /**
+   * Gets the collection of entities.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object.
+   *
+   * @return \Drupal\jsonapi\ResourceResponse
+   *   The response.
+   */
+  public function getCollection(Request $request) {
+    // Instantiate the query for the filtering.
+    $entity_type_id = $this->resourceType->getEntityTypeId();
+
+    $route_params = $request->attributes->get('_route_params');
+    $params = isset($route_params['_json_api_params']) ? $route_params['_json_api_params'] : [];
+    $query = $this->getCollectionQuery($entity_type_id, $params);
+
+    $results = $query->execute();
+
+    $storage = $this->entityTypeManager->getStorage($entity_type_id);
+    // We request N+1 items to find out if there is a next page for the pager. We may need to remove that extra item
+    // before loading the entities.
+    $pager_size = $query->getMetaData('pager_size');
+    if ($has_next_page = $pager_size < count($results)) {
+      // Drop the last result.
+      array_pop($results);
+    }
+    // Each item of the collection data contains an array with 'entity' and
+    // 'access' elements.
+    $collection_data = $this->loadEntitiesWithAccess($storage, $results);
+    $entity_collection = new EntityCollection(array_column($collection_data, 'entity'));
+    $entity_collection->setHasNextPage($has_next_page);
+
+    // Calculate all the results and pass them to the EntityCollectionInterface.
+    if ($this->resourceType->includeCount()) {
+      $total_results = $this
+        ->getCollectionCountQuery($entity_type_id, $params)
+        ->count()
+        ->execute();
+
+      $entity_collection->setTotalCount($total_results);
+    }
+
+    $response = $this->respondWithCollection($entity_collection, $entity_type_id);
+
+    // Add cacheable metadata for the access result.
+    $access_info = array_column($collection_data, 'access');
+    array_walk($access_info, function ($access) use ($response) {
+      $response->addCacheableDependency($access);
+    });
+
+    return $response;
+  }
+
+  /**
+   * Gets the related resource.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The requested entity.
+   * @param string $related_field
+   *   The related field name.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object.
+   *
+   * @return \Drupal\jsonapi\ResourceResponse
+   *   The response.
+   */
+  public function getRelated(EntityInterface $entity, $related_field, Request $request) {
+    $related_field = $this->resourceType->getInternalName($related_field);
+    if (!($field_list = $entity->get($related_field)) || !$this->isRelationshipField($field_list)) {
+      throw new NotFoundHttpException(sprintf('The relationship %s is not present in this resource.', $related_field));
+    }
+    // Add the cacheable metadata from the host entity.
+    $cacheable_metadata = CacheableMetadata::createFromObject($entity);
+    /* @var \Drupal\Core\Field\EntityReferenceFieldItemList $field_list */
+    $is_multiple = $field_list
+      ->getDataDefinition()
+      ->getFieldStorageDefinition()
+      ->isMultiple();
+    if (!$is_multiple) {
+      $response = $this->getIndividual($field_list->entity, $request);
+      // Add cacheable metadata for host entity to individual response.
+      $response->addCacheableDependency($cacheable_metadata);
+      return $response;
+    }
+    $collection_data = [];
+    foreach ($field_list->referencedEntities() as $referenced_entity) {
+      /* @var \Drupal\Core\Entity\EntityInterface $referenced_entity */
+      $collection_data[$referenced_entity->id()] = static::getEntityAndAccess($referenced_entity);
+      $cacheable_metadata->addCacheableDependency($referenced_entity);
+    }
+    $entity_collection = new EntityCollection(array_column($collection_data, 'entity'));
+    $response = $this->buildWrappedResponse($entity_collection);
+
+    $access_info = array_column($collection_data, 'access');
+    array_walk($access_info, function ($access) use ($response) {
+      $response->addCacheableDependency($access);
+    });
+    // $response does not contain the entity list cache tag. We add the
+    // cacheable metadata for the finite list of entities in the relationship.
+    $response->addCacheableDependency($cacheable_metadata);
+
+    return $response;
+  }
+
+  /**
+   * Gets the relationship of an entity.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The requested entity.
+   * @param string $related_field
+   *   The related field name.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object.
+   * @param int $response_code
+   *   The response code. Defaults to 200.
+   *
+   * @return \Drupal\jsonapi\ResourceResponse
+   *   The response.
+   */
+  public function getRelationship(EntityInterface $entity, $related_field, Request $request, $response_code = 200) {
+    $related_field = $this->resourceType->getInternalName($related_field);
+    if (!($field_list = $entity->get($related_field)) || !$this->isRelationshipField($field_list)) {
+      throw new NotFoundHttpException(sprintf('The relationship %s is not present in this resource.', $related_field));
+    }
+    $response = $this->buildWrappedResponse($field_list, $response_code);
+    return $response;
+  }
+
+  /**
+   * Adds a relationship to a to-many relationship.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The requested entity.
+   * @param string $related_field
+   *   The related field name.
+   * @param mixed $parsed_field_list
+   *   The entity reference field list of items to add, or a response object in
+   *   case of error.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object.
+   *
+   * @return \Drupal\jsonapi\ResourceResponse
+   *   The response.
+   */
+  public function createRelationship(EntityInterface $entity, $related_field, $parsed_field_list, Request $request) {
+    $related_field = $this->resourceType->getInternalName($related_field);
+    if ($parsed_field_list instanceof Response) {
+      // This usually means that there was an error, so there is no point on
+      // processing further.
+      return $parsed_field_list;
+    }
+    /* @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $parsed_field_list */
+    $this->relationshipAccess($entity, $related_field);
+    // According to the specification, you are only allowed to POST to a
+    // relationship if it is a to-many relationship.
+    /* @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $field_list */
+    $field_list = $entity->{$related_field};
+    $is_multiple = $field_list->getFieldDefinition()
+      ->getFieldStorageDefinition()
+      ->isMultiple();
+    if (!$is_multiple) {
+      throw new ConflictHttpException(sprintf('You can only POST to to-many relationships. %s is a to-one relationship.', $related_field));
+    }
+
+    $field_access = $field_list->access('edit', NULL, TRUE);
+    if (!$field_access->isAllowed()) {
+      $field_name = $field_list->getName();
+      throw new EntityAccessDeniedHttpException($entity, $field_access, '/data/relationships/' . $field_name, sprintf('The current user is not allowed to PATCH the selected field (%s).', $field_name));
+    }
+    // Time to save the relationship.
+    foreach ($parsed_field_list as $field_item) {
+      $field_list->appendItem($field_item->getValue());
+    }
+    $this->validate($entity);
+    $entity->save();
+    return $this->getRelationship($entity, $related_field, $request, 201);
+  }
+
+  /**
+   * Updates the relationship of an entity.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The requested entity.
+   * @param string $related_field
+   *   The related field name.
+   * @param mixed $parsed_field_list
+   *   The entity reference field list of items to add, or a response object in
+   *   case of error.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object.
+   *
+   * @return \Drupal\jsonapi\ResourceResponse
+   *   The response.
+   */
+  public function patchRelationship(EntityInterface $entity, $related_field, $parsed_field_list, Request $request) {
+    $related_field = $this->resourceType->getInternalName($related_field);
+    if ($parsed_field_list instanceof Response) {
+      // This usually means that there was an error, so there is no point on
+      // processing further.
+      return $parsed_field_list;
+    }
+    /* @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $parsed_field_list */
+    $this->relationshipAccess($entity, $related_field);
+    // According to the specification, PATCH works a little bit different if the
+    // relationship is to-one or to-many.
+    /* @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $field_list */
+    $field_list = $entity->{$related_field};
+    $is_multiple = $field_list->getFieldDefinition()
+      ->getFieldStorageDefinition()
+      ->isMultiple();
+    $method = $is_multiple ? 'doPatchMultipleRelationship' : 'doPatchIndividualRelationship';
+    $this->{$method}($entity, $parsed_field_list);
+    $this->validate($entity);
+    $entity->save();
+    return $this->getRelationship($entity, $related_field, $request);
+  }
+
+  /**
+   * Update a to-one relationship.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The requested entity.
+   * @param \Drupal\Core\Field\EntityReferenceFieldItemListInterface $parsed_field_list
+   *   The entity reference field list of items to add, or a response object in
+   *   case of error.
+   */
+  protected function doPatchIndividualRelationship(EntityInterface $entity, EntityReferenceFieldItemListInterface $parsed_field_list) {
+    if ($parsed_field_list->count() > 1) {
+      throw new BadRequestHttpException(sprintf('Provide a single relationship so to-one relationship fields (%s).', $parsed_field_list->getName()));
+    }
+    $this->doPatchMultipleRelationship($entity, $parsed_field_list);
+  }
+
+  /**
+   * Update a to-many relationship.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The requested entity.
+   * @param \Drupal\Core\Field\EntityReferenceFieldItemListInterface $parsed_field_list
+   *   The entity reference field list of items to add, or a response object in
+   *   case of error.
+   */
+  protected function doPatchMultipleRelationship(EntityInterface $entity, EntityReferenceFieldItemListInterface $parsed_field_list) {
+    $field_name = $parsed_field_list->getName();
+    $field_access = $parsed_field_list->access('edit', NULL, TRUE);
+    if (!$field_access->isAllowed()) {
+      throw new EntityAccessDeniedHttpException($entity, $field_access, '/data/relationships/' . $field_name, sprintf('The current user is not allowed to PATCH the selected field (%s).', $field_name));
+    }
+    $entity->{$field_name} = $parsed_field_list;
+  }
+
+  /**
+   * Deletes the relationship of an entity.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The requested entity.
+   * @param string $related_field
+   *   The related field name.
+   * @param mixed $parsed_field_list
+   *   The entity reference field list of items to add, or a response object in
+   *   case of error.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object.
+   *
+   * @return \Drupal\jsonapi\ResourceResponse
+   *   The response.
+   */
+  public function deleteRelationship(EntityInterface $entity, $related_field, $parsed_field_list, Request $request = NULL) {
+    if ($parsed_field_list instanceof Response) {
+      // This usually means that there was an error, so there is no point on
+      // processing further.
+      return $parsed_field_list;
+    }
+    if ($parsed_field_list instanceof Request) {
+      // This usually means that there was not body provided.
+      throw new BadRequestHttpException(sprintf('You need to provide a body for DELETE operations on a relationship (%s).', $related_field));
+    }
+    /* @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $parsed_field_list */
+    $this->relationshipAccess($entity, $related_field);
+
+    $field_name = $parsed_field_list->getName();
+    $field_access = $parsed_field_list->access('edit', NULL, TRUE);
+    if (!$field_access->isAllowed()) {
+      throw new EntityAccessDeniedHttpException($entity, $field_access, '/data/relationships/' . $field_name, sprintf('The current user is not allowed to PATCH the selected field (%s).', $field_name));
+    }
+    /* @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $field_list */
+    $field_list = $entity->{$related_field};
+    $is_multiple = $field_list->getFieldDefinition()
+      ->getFieldStorageDefinition()
+      ->isMultiple();
+    if (!$is_multiple) {
+      throw new ConflictHttpException(sprintf('You can only DELETE from to-many relationships. %s is a to-one relationship.', $related_field));
+    }
+
+    // Compute the list of current values and remove the ones in the payload.
+    $current_values = $field_list->getValue();
+    $deleted_values = $parsed_field_list->getValue();
+    $keep_values = array_udiff($current_values, $deleted_values, function ($first, $second) {
+      return reset($first) - reset($second);
+    });
+    // Replace the existing field with one containing the relationships to keep.
+    $entity->{$related_field} = $this->pluginManager
+      ->createFieldItemList($entity, $related_field, $keep_values);
+
+    // Save the entity and return the response object.
+    $this->validate($entity);
+    $entity->save();
+    return $this->getRelationship($entity, $related_field, $request, 201);
+  }
+
+  /**
+   * Gets a basic query for a collection.
+   *
+   * @param string $entity_type_id
+   *   The entity type for the entity query.
+   * @param \Drupal\jsonapi\Routing\Param\JsonApiParamInterface[] $params
+   *   The parameters for the query.
+   *
+   * @return \Drupal\Core\Entity\Query\QueryInterface
+   *   A new query.
+   */
+  protected function getCollectionQuery($entity_type_id, $params) {
+    $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
+
+    $query = $this->queryBuilder->newQuery($entity_type, $params);
+
+    // Limit this query to the bundle type for this resource.
+    $bundle = $this->resourceType->getBundle();
+    if ($bundle && ($bundle_key = $entity_type->getKey('bundle'))) {
+      $query->condition(
+        $bundle_key, $bundle
+      );
+    }
+
+    return $query;
+  }
+
+  /**
+   * Gets a basic query for a collection count.
+   *
+   * @param string $entity_type_id
+   *   The entity type for the entity query.
+   * @param \Drupal\jsonapi\Routing\Param\JsonApiParamInterface[] $params
+   *   The parameters for the query.
+   *
+   * @return \Drupal\Core\Entity\Query\QueryInterface
+   *   A new query.
+   */
+  protected function getCollectionCountQuery($entity_type_id, $params) {
+    // Override the pagination parameter to get all the available results.
+    $params[OffsetPage::KEY_NAME] = new JsonApiParamBase([]);
+    return $this->getCollectionQuery($entity_type_id, $params);
+  }
+
+  /**
+   * Builds a response with the appropriate wrapped document.
+   *
+   * @param mixed $data
+   *   The data to wrap.
+   * @param int $response_code
+   *   The response code.
+   * @param array $headers
+   *   An array of response headers.
+   *
+   * @return \Drupal\jsonapi\ResourceResponse
+   *   The response.
+   */
+  protected function buildWrappedResponse($data, $response_code = 200, array $headers = []) {
+    return new ResourceResponse(new JsonApiDocumentTopLevel($data), $response_code, $headers);
+  }
+
+  /**
+   * Respond with an entity collection.
+   *
+   * @param \Drupal\jsonapi\EntityCollection $entity_collection
+   *   The collection of entites.
+   * @param string $entity_type_id
+   *   The entity type.
+   *
+   * @return \Drupal\jsonapi\ResourceResponse
+   *   The response.
+   */
+  protected function respondWithCollection(EntityCollection $entity_collection, $entity_type_id) {
+    $response = $this->buildWrappedResponse($entity_collection);
+
+    // When a new change to any entity in the resource happens, we cannot ensure
+    // the validity of this cached list. Add the list tag to deal with that.
+    $list_tag = $this->entityTypeManager->getDefinition($entity_type_id)
+      ->getListCacheTags();
+    $response->getCacheableMetadata()->addCacheTags($list_tag);
+    return $response;
+  }
+
+  /**
+   * Check the access to update the entity and the presence of a relationship.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity.
+   * @param string $related_field
+   *   The name of the field to check.
+   */
+  protected function relationshipAccess(EntityInterface $entity, $related_field) {
+    /* @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $parsed_field_list */
+    $entity_access = $entity->access('update', NULL, TRUE);
+    if (!$entity_access->isAllowed()) {
+      // @todo Is this really the right path?
+      throw new EntityAccessDeniedHttpException($entity, $entity_access, $related_field, 'The current user is not allowed to update the selected resource.');
+    }
+    if (!($field_list = $entity->get($related_field)) || !$this->isRelationshipField($field_list)) {
+      throw new NotFoundHttpException(sprintf('The relationship %s is not present in this resource.', $related_field));
+    }
+  }
+
+  /**
+   * Takes a field from the origin entity and puts it to the destination entity.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $origin
+   *   The entity that contains the field values.
+   * @param \Drupal\Core\Entity\EntityInterface $destination
+   *   The entity that needs to be updated.
+   * @param string $field_name
+   *   The name of the field to extract and update.
+   */
+  protected function updateEntityField(EntityInterface $origin, EntityInterface $destination, $field_name) {
+    // The update is different for configuration entities and content entities.
+    if ($origin instanceof ContentEntityInterface && $destination instanceof ContentEntityInterface) {
+      // First scenario: both are content entities.
+      try {
+        $field_name = $this->resourceType->getInternalName($field_name);
+        $destination_field_list = $destination->get($field_name);
+      }
+      catch (\Exception $e) {
+        throw new BadRequestHttpException(sprintf('The provided field (%s) does not exist in the entity with ID %s.', $field_name, $destination->uuid()));
+      }
+
+      $origin_field_list = $origin->get($field_name);
+      if ($destination_field_list->getValue() != $origin_field_list->getValue()) {
+        $field_access = $destination_field_list->access('edit', NULL, TRUE);
+        if (!$field_access->isAllowed()) {
+          throw new EntityAccessDeniedHttpException($destination, $field_access, '/data/attributes/' . $field_name, sprintf('The current user is not allowed to PATCH the selected field (%s).', $field_name));
+        }
+        $destination->{$field_name} = $origin->get($field_name);
+      }
+    }
+    elseif ($origin instanceof ConfigEntityInterface && $destination instanceof ConfigEntityInterface) {
+      // Second scenario: both are config entities.
+      $destination->set($field_name, $origin->get($field_name));
+    }
+    else {
+      throw new BadRequestHttpException('The serialized entity and the destination entity are of different types.');
+    }
+  }
+
+  /**
+   * Checks if is a relationship field.
+   *
+   * @param object $entity_field
+   *   Entity definition.
+   *
+   * @return bool
+   *   Returns TRUE, if entity field is EntityReferenceItem.
+   */
+  protected function isRelationshipField($entity_field) {
+    $class = $this->pluginManager->getPluginClass($entity_field->getDataDefinition()->getType());
+    return ($class == EntityReferenceItem::class || is_subclass_of($class, EntityReferenceItem::class));
+  }
+
+  /**
+   * Build a collection of the entities to respond with and access objects.
+   *
+   * @param \Drupal\Core\Entity\EntityStorageInterface $storage
+   *   The entity storage to load the entities from.
+   * @param int[] $ids
+   *   Array of entity IDs.
+   *
+   * @return array
+   *   An array keyed by entity ID containing the keys:
+   *     - entity: the loaded entity or an access exception.
+   *     - access: the access object.
+   */
+  protected function loadEntitiesWithAccess(EntityStorageInterface $storage, $ids) {
+    $output = [];
+    foreach ($storage->loadMultiple($ids) as $entity) {
+      $output[$entity->id()] = static::getEntityAndAccess($entity);
+    }
+    return $output;
+  }
+
+  /**
+   * Get the object to normalize and the access based on the provided entity.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity to test access for.
+   *
+   * @return array
+   *   An array containing the keys:
+   *     - entity: the loaded entity or an access exception.
+   *     - access: the access object.
+   */
+  public static function getEntityAndAccess(EntityInterface $entity) {
+    /** @var \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository */
+    $entity_repository = \Drupal::service('entity.repository');
+    $entity = $entity_repository->getTranslationFromContext($entity, NULL, ['operation' => 'entity_upcast']);
+    $access = $entity->access('view', NULL, TRUE);
+    // Accumulate the cacheability metadata for the access.
+    $output = [
+      'access' => $access,
+      'entity' => $entity,
+    ];
+    if ($entity instanceof AccessibleInterface && !$access->isAllowed()) {
+      // Pass an exception to the list of things to normalize.
+      $output['entity'] = new EntityAccessDeniedHttpException($entity, $access, '/data', 'The current user is not allowed to GET the selected resource.');
+    }
+
+    return $output;
+  }
+
+  /**
+   * Checks if the given entity exists.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity for which to test existence.
+   *
+   * @return bool
+   *   Whether the entity already has been created.
+   */
+  protected function entityExists(EntityInterface $entity) {
+    $entity_storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
+    return !empty($entity_storage->loadByProperties([
+      'uuid' => $entity->uuid(),
+    ]));
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Controller/EntryPoint.php b/drupal/modules/jsonapi/src/Controller/EntryPoint.php
new file mode 100644
index 0000000..85c053e
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Controller/EntryPoint.php
@@ -0,0 +1,92 @@
+<?php
+
+namespace Drupal\jsonapi\Controller;
+
+use Drupal\Core\Cache\CacheableJsonResponse;
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Render\RenderContext;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\Core\Url;
+use Drupal\jsonapi\ResourceType\ResourceType;
+use Drupal\jsonapi\ResourceType\ResourceTypeRepository;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * @internal
+ */
+class EntryPoint extends ControllerBase {
+
+  /**
+   * @var \Drupal\jsonapi\ResourceType\ResourceTypeRepository
+   */
+  protected $resourceTypeRepository;
+
+  /**
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * EntryPoint constructor.
+   *
+   * @param \Drupal\jsonapi\ResourceType\ResourceTypeRepository $resource_type_repository
+   *   The resource type repository.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer.
+   */
+  public function __construct(ResourceTypeRepository $resource_type_repository, RendererInterface $renderer) {
+    $this->resourceTypeRepository = $resource_type_repository;
+    $this->renderer = $renderer;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('jsonapi.resource_type.repository'),
+      $container->get('renderer')
+    );
+  }
+
+  /**
+   * Controller to list all the resources.
+   *
+   * @return \Drupal\Core\Cache\CacheableJsonResponse
+   */
+  public function index() {
+    // Execute the request in context so the cacheable metadata from the entity
+    // grants system is caught and added to the response. This is surfaced when
+    // executing the underlying entity query.
+    $context = new RenderContext();
+    /** @var \Drupal\Core\Cache\CacheableResponseInterface $response */
+    $do_build_urls = function () {
+      $self = Url::fromRoute('jsonapi.resource_list')->setAbsolute();
+
+      return array_reduce($this->resourceTypeRepository->all(), function (array $carry, ResourceType $resource_type) {
+        // TODO: Learn how to invalidate the cache for this page when a new entity
+        // type or bundle gets added, removed or updated.
+        // $this->response->addCacheableDependency($definition);
+        $url = Url::fromRoute(sprintf('jsonapi.%s.collection', $resource_type->getTypeName()))
+          ->setAbsolute();
+        $carry[$resource_type->getTypeName()] = $url->toString();
+
+        return $carry;
+      }, ['self' => $self->toString()]);
+    };
+    $urls = $this->renderer->executeInRenderContext($context, $do_build_urls);
+
+    $json_response = new CacheableJsonResponse([
+      'data' => [],
+      'links' => $urls,
+    ]
+    );
+
+    if (!$context->isEmpty()) {
+      $json_response->addCacheableDependency($context->pop());
+    }
+
+    return $json_response;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Controller/RequestHandler.php b/drupal/modules/jsonapi/src/Controller/RequestHandler.php
new file mode 100644
index 0000000..90683d7
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Controller/RequestHandler.php
@@ -0,0 +1,218 @@
+<?php
+
+namespace Drupal\jsonapi\Controller;
+
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Render\RenderContext;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\jsonapi\Context\CurrentContext;
+use Symfony\Component\DependencyInjection\ContainerAwareInterface;
+use Symfony\Component\DependencyInjection\ContainerAwareTrait;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Serializer\Exception\UnexpectedValueException;
+use Symfony\Component\Serializer\SerializerInterface;
+
+/**
+ * Acts as intermediate request forwarder for resource plugins.
+ *
+ * @internal
+ */
+class RequestHandler implements ContainerAwareInterface, ContainerInjectionInterface {
+
+  use ContainerAwareTrait;
+
+  protected static $requiredCacheContexts = ['user.permissions'];
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static();
+  }
+
+  /**
+   * Handles a web API request.
+   *
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The route match.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The HTTP request object.
+   *
+   * @return \Drupal\Core\Cache\CacheableResponseInterface
+   *   The response object.
+   */
+  public function handle(RouteMatchInterface $route_match, Request $request) {
+    $method = strtolower($request->getMethod());
+    $route = $route_match->getRouteObject();
+
+    // Deserialize incoming data if available.
+    /* @var \Symfony\Component\Serializer\SerializerInterface $serializer */
+    $serializer = $this->container->get('serializer');
+    /* @var \Drupal\jsonapi\Context\CurrentContext $current_context */
+    $current_context = $this->container->get('jsonapi.current_context');
+    $current_context->reset();
+    $unserialized = $this->deserializeBody($request, $serializer, $route->getOption('serialization_class'), $current_context);
+    if ($unserialized instanceof Response && !$unserialized->isSuccessful()) {
+      return $unserialized;
+    }
+
+    // Determine the request parameters that should be passed to the resource
+    // plugin.
+    $route_parameters = $route_match->getParameters();
+    $parameters = [];
+
+    // Filter out all internal parameters starting with "_".
+    foreach ($route_parameters as $key => $parameter) {
+      if ($key{0} !== '_') {
+        $parameters[] = $parameter;
+      }
+    }
+
+    // Invoke the operation on the resource plugin.
+    $action = $this->action($route_match, $method);
+    $resource = $this->resourceFactory($route, $current_context);
+
+    // Only add the unserialized data if there is something there.
+    $extra_parameters = $unserialized ? [$unserialized, $request] : [$request];
+
+    // Execute the request in context so the cacheable metadata from the entity
+    // grants system is caught and added to the response. This is surfaced when
+    // executing the underlying entity query.
+    $context = new RenderContext();
+    /** @var \Drupal\Core\Cache\CacheableResponseInterface $response */
+    $response = $this->container->get('renderer')
+      ->executeInRenderContext($context, function () use ($resource, $action, $parameters, $extra_parameters) {
+        return call_user_func_array([$resource, $action], array_merge($parameters, $extra_parameters));
+      });
+    if (!$context->isEmpty()) {
+      $response->addCacheableDependency($context->pop());
+    }
+
+    return $response;
+  }
+
+  /**
+   * Deserializes the sent data.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request.
+   * @param \Symfony\Component\Serializer\SerializerInterface $serializer
+   *   The serializer for the deserialization of the input data.
+   * @param string $serialization_class
+   *   The class the input data needs to deserialize into.
+   * @param \Drupal\jsonapi\Context\CurrentContext $current_context
+   *   The current context.
+   *
+   * @return mixed
+   *   The deserialized data or a Response object in case of error.
+   */
+  public function deserializeBody(Request $request, SerializerInterface $serializer, $serialization_class, CurrentContext $current_context) {
+    $received = $request->getContent();
+    if (empty($received) || $request->isMethodSafe()) {
+      return NULL;
+    }
+    $format = $request->getContentType();
+    try {
+      return $serializer->deserialize($received, $serialization_class, $format, [
+        'related' => $request->get('related'),
+        'target_entity' => $request->get($current_context->getResourceType()->getEntityTypeId()),
+        'resource_type' => $current_context->getResourceType(),
+      ]);
+    }
+    catch (UnexpectedValueException $e) {
+      throw new UnprocessableEntityHttpException(
+        sprintf('There was an error un-serializing the data. Message: %s.', $e->getMessage()),
+        $e
+      );
+    }
+  }
+
+  /**
+   * Gets the method to execute in the entity resource.
+   *
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The route match.
+   * @param string $method
+   *   The lowercase HTTP method.
+   *
+   * @return string
+   *   The method to execute in the EntityResource.
+   */
+  protected function action(RouteMatchInterface $route_match, $method) {
+    $on_relationship = ($route_match->getRouteObject()->getDefault('_on_relationship'));
+    switch ($method) {
+      case 'get':
+        if ($on_relationship) {
+          return 'getRelationship';
+        }
+        elseif ($route_match->getParameter('related')) {
+          return 'getRelated';
+        }
+        return $this->getEntity($route_match) ? 'getIndividual' : 'getCollection';
+
+      case 'post':
+        return ($on_relationship) ? 'createRelationship' : 'createIndividual';
+
+      case 'patch':
+        return ($on_relationship) ? 'patchRelationship' : 'patchIndividual';
+
+      case 'delete':
+        return ($on_relationship) ? 'deleteRelationship' : 'deleteIndividual';
+    }
+  }
+
+  /**
+   * Gets the entity for the operation.
+   *
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The matched route.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface
+   *   The upcasted entity.
+   */
+  protected function getEntity(RouteMatchInterface $route_match) {
+    $route = $route_match->getRouteObject();
+    return $route_match->getParameter($route->getRequirement('_entity_type'));
+  }
+
+  /**
+   * Get the resource.
+   *
+   * @param \Symfony\Component\Routing\Route $route
+   *   The matched route.
+   * @param \Drupal\jsonapi\Context\CurrentContext $current_context
+   *   The current context.
+   *
+   * @return \Drupal\jsonapi\Controller\EntityResource
+   *   The instantiated resource.
+   */
+  protected function resourceFactory(Route $route, CurrentContext $current_context) {
+    /** @var \Drupal\jsonapi\ResourceType\ResourceTypeRepository $resource_type_repository */
+    $resource_type_repository = $this->container->get('jsonapi.resource_type.repository');
+    /* @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
+    $entity_type_manager = $this->container->get('entity_type.manager');
+    /* @var \Drupal\jsonapi\Query\QueryBuilder $query_builder */
+    $query_builder = $this->container->get('jsonapi.query_builder');
+    /* @var \Drupal\Core\Entity\EntityFieldManagerInterface $field_manager */
+    $field_manager = $this->container->get('entity_field.manager');
+    /* @var \Drupal\Core\Field\FieldTypePluginManagerInterface $plugin_manager */
+    $plugin_manager = $this->container->get('plugin.manager.field.field_type');
+    /** @var \Drupal\jsonapi\LinkManager\LinkManager $link_manager */
+    $link_manager = $this->container->get('jsonapi.link_manager');
+    $resource = new EntityResource(
+      $resource_type_repository->get($route->getRequirement('_entity_type'), $route->getRequirement('_bundle')),
+      $entity_type_manager,
+      $query_builder,
+      $field_manager,
+      $current_context,
+      $plugin_manager,
+      $link_manager
+    );
+    return $resource;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/DependencyInjection/Compiler/RemoveJsonapiFormatCompilerPass.php b/drupal/modules/jsonapi/src/DependencyInjection/Compiler/RemoveJsonapiFormatCompilerPass.php
new file mode 100644
index 0000000..535038a
--- /dev/null
+++ b/drupal/modules/jsonapi/src/DependencyInjection/Compiler/RemoveJsonapiFormatCompilerPass.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Drupal\jsonapi\DependencyInjection\Compiler;
+
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+
+/**
+ * Removes 'api_json' format from the 'serializer.formats' container parameter.
+ *
+ * We want the 'api_json' format to not be supported in the REST module. But the
+ * JSON API module also should not have to define al alternative 'serializer'
+ * service.
+ * This is achieved through removing the 'api_json' format from the
+ * 'serializer.formats' container parameter. The consequences of doing that:
+ *
+ * - the REST module no longer allows this format to be used
+ * - the 'serialization.exception.default' service does not support 'api_json',
+ *   hence a custom exception subscriber is needed, which this module has:
+ *   'jsonapi.exception_subscriber'
+ * - the 'serializer' service does support 'api_json'
+ *
+ * In other words: the 'serializer' service supports 'api_json', but nothing is
+ * aware of it. You could only know by calling 'serializer:supportsEncoding()'.
+ *
+ * @see \Drupal\serialization\RegisterSerializationClassesCompilerPass
+ * @see \Drupal\jsonapi\JsonapiServiceProvider::register()
+ * @see \Drupal\jsonapi\EventSubscriber\DefaultExceptionSubscriber
+ * @see \Drupal\Tests\jsonapi\Functional\RestJsonApiUnsupported
+ */
+class RemoveJsonapiFormatCompilerPass implements CompilerPassInterface {
+
+  /**
+   * Updates the 'serializer.formats' container parameter.
+   *
+   * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
+   *   The container to process.
+   */
+  public function process(ContainerBuilder $container) {
+    if ($container->hasParameter('serializer.formats')) {
+      $filtered_formats = array_filter(
+        $container->getParameter('serializer.formats'),
+        function ($format) {
+          return $format !== 'api_json';
+        }
+      );
+      $container->setParameter('serializer.formats', array_values($filtered_formats));
+    }
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Encoder/JsonEncoder.php b/drupal/modules/jsonapi/src/Encoder/JsonEncoder.php
new file mode 100644
index 0000000..25000e8
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Encoder/JsonEncoder.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Drupal\jsonapi\Encoder;
+
+use Drupal\jsonapi\Normalizer\Value\ValueExtractorInterface;
+use Drupal\serialization\Encoder\JsonEncoder as SerializationJsonEncoder;
+
+/**
+ * Encodes JSON API data.
+ *
+ * @internal
+ */
+class JsonEncoder extends SerializationJsonEncoder {
+
+  /**
+   * The formats that this Encoder supports.
+   *
+   * @var string
+   */
+  protected static $format = ['api_json'];
+
+  /**
+   * {@inheritdoc}
+   *
+   * @see http://jsonapi.org/format/#errors
+   */
+  public function encode($data, $format, array $context = []) {
+    // Make sure that any auto-normalizable object gets normalized before
+    // encoding. This is specially important to generate the errors in partial
+    // success responses.
+    if ($data instanceof ValueExtractorInterface) {
+      $data = $data->rasterizeValue();
+    }
+    // Allows wrapping the encoded output. This is so we can use the same
+    // encoder and normalizers when serializing HttpExceptions to match the
+    // JSON API specification.
+    if (!empty($context['data_wrapper'])) {
+      $data = [$context['data_wrapper'] => $data];
+    }
+    return parent::encode($data, $format, $context);
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/EntityToJsonApi.php b/drupal/modules/jsonapi/src/EntityToJsonApi.php
new file mode 100644
index 0000000..35e534c
--- /dev/null
+++ b/drupal/modules/jsonapi/src/EntityToJsonApi.php
@@ -0,0 +1,107 @@
+<?php
+
+namespace Drupal\jsonapi;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Session\AccountProxyInterface;
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\jsonapi\Resource\JsonApiDocumentTopLevel;
+use Drupal\jsonapi\ResourceType\ResourceTypeRepository;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Serializer\Serializer;
+
+/**
+ * Simplifies the process of generating a JSON API version of an entity.
+ */
+class EntityToJsonApi {
+
+  /**
+   * The currently logged in user.
+   *
+   * @var \Drupal\Core\Session\AccountProxyInterface
+   */
+  protected $currentUser;
+
+  /**
+   * Serializer object.
+   *
+   * @var \Symfony\Component\Serializer\Serializer
+   */
+  protected $serializer;
+
+  /**
+   * @var \Drupal\jsonapi\ResourceType\ResourceTypeRepository
+   */
+  protected $resourceTypeRepository;
+
+  /**
+   * EntityToJsonApi constructor.
+   *
+   * @param \Symfony\Component\Serializer\Serializer $serializer
+   *   The serializer.
+   * @param \Drupal\Core\Session\AccountProxyInterface $current_user
+   *   The currently logged in user.
+   */
+  public function __construct(Serializer $serializer, ResourceTypeRepository $resource_type_repository, AccountProxyInterface $current_user) {
+    $this->serializer = $serializer;
+    $this->resourceTypeRepository = $resource_type_repository;
+    $this->currentUser = $current_user;
+  }
+
+  /**
+   * Return the requested entity as a raw string.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity to generate the JSON from.
+   *
+   * @return string
+   *   The raw JSON string of the requested resource.
+   */
+  public function serialize(EntityInterface $entity) {
+    // TODO: Supporting includes requires adding the 'include' query string.
+    return $this->serializer->serialize(new JsonApiDocumentTopLevel($entity),
+      'api_json',
+      $this->calculateContext($entity)
+    );
+  }
+
+  /**
+   * Return the requested entity as an structured array.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity to generate the JSON from.
+   *
+   * @return array
+   *   The JSON structure of the requested resource.
+   */
+  public function normalize(EntityInterface $entity) {
+    return $this->serializer->normalize(new JsonApiDocumentTopLevel($entity),
+      'api_json',
+      $this->calculateContext($entity)
+    );
+  }
+
+  /**
+   * Calculate the context for the serialize/normalize operation.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity to generate the JSON from.
+   *
+   * @return array
+   *   The context.
+   */
+  protected function calculateContext(EntityInterface $entity) {
+    // TODO: Supporting includes requires adding the 'include' query string.
+    $request = new Request();
+    return [
+      'account' => $this->currentUser,
+      'cacheable_metadata' => new CacheableMetadata(),
+      'resource_type' => $this->resourceTypeRepository->get(
+        $entity->getEntityTypeId(),
+        $entity->bundle()
+      ),
+      'request' => $request,
+    ];
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/EventSubscriber/DefaultExceptionSubscriber.php b/drupal/modules/jsonapi/src/EventSubscriber/DefaultExceptionSubscriber.php
new file mode 100644
index 0000000..6da6c50
--- /dev/null
+++ b/drupal/modules/jsonapi/src/EventSubscriber/DefaultExceptionSubscriber.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace Drupal\jsonapi\EventSubscriber;
+
+use Drupal\serialization\EventSubscriber\DefaultExceptionSubscriber as SerializationDefaultExceptionSubscriber;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
+use Symfony\Component\HttpKernel\Exception\HttpException;
+
+/**
+ * @internal
+ */
+class DefaultExceptionSubscriber extends SerializationDefaultExceptionSubscriber {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static function getPriority() {
+    return parent::getPriority() + 25;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getHandledFormats() {
+    return ['api_json'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onException(GetResponseForExceptionEvent $event) {
+    /** @var \Symfony\Component\HttpKernel\Exception\HttpException $exception */
+    $exception = $event->getException();
+    if (!$this->isJsonApiFormatted($event->getRequest())) {
+      return;
+    }
+    if (!$exception instanceof HttpException) {
+      $exception = new HttpException(500, $exception->getMessage(), $exception);
+      $event->setException($exception);
+    }
+
+    $this->setEventResponse($event, $exception->getStatusCode());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setEventResponse(GetResponseForExceptionEvent $event, $status) {
+    /** @var \Symfony\Component\HttpKernel\Exception\HttpException $exception */
+    $exception = $event->getException();
+    if (!$this->isJsonApiFormatted($event->getRequest())) {
+      return;
+    }
+    $encoded_content = $this->serializer->serialize($exception, 'api_json', ['data_wrapper' => 'errors']);
+    $response = new Response($encoded_content, $status);
+    $response->headers->set('Content-Type', 'application/vnd.api+json');
+    $event->setResponse($response);
+  }
+
+  /**
+   * Check if the error should be formatted using JSON API.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The failed request.
+   *
+   * @return bool
+   *   TRUE if it needs to be formated using JSON API. FALSE otherwise.
+   */
+  protected function isJsonApiFormatted(Request $request) {
+    $route = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT);
+    $format = $request->getRequestFormat();
+    // The JSON API format is supported if the format is explicitly set or the
+    // request is for a known JSON API route.
+    return $format === 'api_json' || ($route && $route->getOption('_is_jsonapi'));
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/EventSubscriber/ResourceResponseSubscriber.php b/drupal/modules/jsonapi/src/EventSubscriber/ResourceResponseSubscriber.php
new file mode 100644
index 0000000..ce8d1f2
--- /dev/null
+++ b/drupal/modules/jsonapi/src/EventSubscriber/ResourceResponseSubscriber.php
@@ -0,0 +1,215 @@
+<?php
+
+namespace Drupal\jsonapi\EventSubscriber;
+
+use JsonSchema\Validator;
+use Drupal\Component\Serialization\Json;
+use Drupal\Core\Cache\CacheableResponse;
+use Drupal\Core\Cache\CacheableResponseInterface;
+use Drupal\Core\Render\RenderContext;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\jsonapi\ResourceResponse;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
+use Symfony\Component\HttpKernel\KernelEvents;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\Serializer\SerializerInterface;
+
+/**
+ * Response subscriber that serializes and removes ResourceResponses' data.
+ *
+ * @see \Drupal\rest\EventSubscriber\ResourceResponseSubscriber
+ *
+ * This is 99% identical to \Drupal\rest\EventSubscriber\ResourceResponseSubscriber
+ * but with a few differences:
+ * 1. It has the @jsonapi.serializer service injected instead of @serializer
+ * 2. It has the @current_route_match service no longer injected
+ * 3. It hardcodes the format to 'api_json'
+ * 4. In the call to the serializer, it passes in the request and cacheable
+ *    metadata as serialization context.
+ * 5. It validates the final response according to the JSON API JSON schema
+ * 6. It has a different priority, to ensure it runs before the Dynamic Page
+ *    Cache event subscriber β€” but this should also be fixed in the original
+ *    class, see issue
+ */
+class ResourceResponseSubscriber implements EventSubscriberInterface {
+
+  /**
+   * The serializer.
+   *
+   * @var \Symfony\Component\Serializer\SerializerInterface
+   */
+  protected $serializer;
+
+  /**
+   * The renderer.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * The JSON API logger channel.
+   *
+   * @var \Psr\Log\LoggerInterface
+   */
+  protected $logger;
+
+  /**
+   * Constructs a ResourceResponseSubscriber object.
+   *
+   * @param \Symfony\Component\Serializer\SerializerInterface $serializer
+   *   The serializer.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer.
+   * @param \Psr\Log\LoggerInterface $logger
+   *   The JSON API logger channel.
+   */
+  public function __construct(SerializerInterface $serializer, RendererInterface $renderer, LoggerInterface $logger) {
+    $this->serializer = $serializer;
+    $this->renderer = $renderer;
+    $this->logger = $logger;
+  }
+
+  /**
+   * Serializes ResourceResponse responses' data, and removes that data.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
+   *   The event to process.
+   */
+  public function onResponse(FilterResponseEvent $event) {
+    $response = $event->getResponse();
+    if (!$response instanceof ResourceResponse) {
+      return;
+    }
+
+    $request = $event->getRequest();
+    $format = 'api_json';
+    $this->renderResponseBody($request, $response, $this->serializer, $format);
+    $event->setResponse($this->flattenResponse($response));
+
+    assert('$this->validateResponse($event->getResponse())', 'A JSON API response failed validation (see the logs for details). Please report this in the issue queue on drupal.org');
+  }
+
+  /**
+   * Renders a resource response body.
+   *
+   * Serialization can invoke rendering (e.g., generating URLs), but the
+   * serialization API does not provide a mechanism to collect the
+   * bubbleable metadata associated with that (e.g., language and other
+   * contexts), so instead, allow those to "leak" and collect them here in
+   * a render context.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object.
+   * @param \Drupal\jsonapi\ResourceResponse $response
+   *   The response from the JSON API resource.
+   * @param \Symfony\Component\Serializer\SerializerInterface $serializer
+   *   The serializer to use.
+   * @param string|null $format
+   *   The response format, or NULL in case the response does not need a format,
+   *   for example for the response to a DELETE request.
+   *
+   * @todo Add test coverage for language negotiation contexts in
+   *   https://www.drupal.org/node/2135829.
+   */
+  protected function renderResponseBody(Request $request, ResourceResponse $response, SerializerInterface $serializer, $format) {
+    $data = $response->getResponseData();
+
+    // If there is data to send, serialize and set it as the response body.
+    if ($data !== NULL) {
+      $context = new RenderContext();
+      $output = $this->renderer
+        ->executeInRenderContext($context, function () use ($serializer, $data, $format, $request, $response) {
+          // The serializer receives the response's cacheability metadata object
+          // as serialization context. Normalizers called by the serializer then
+          // refine this cacheability metadata, and thus they are effectively
+          // updating the response object's cacheability.
+          return $serializer->serialize($data, $format, ['request' => $request, 'cacheable_metadata' => $response->getCacheableMetadata()]);
+        });
+
+      if ($response instanceof CacheableResponseInterface && !$context->isEmpty()) {
+        $response->addCacheableDependency($context->pop());
+      }
+
+      $response->setContent($output);
+      $response->headers->set('Content-Type', $request->getMimeType($format));
+    }
+  }
+
+  /**
+   * Flattens a fully rendered resource response.
+   *
+   * Ensures that complex data structures in ResourceResponse::getResponseData()
+   * are not serialized. Not doing this means that caching this response object
+   * requires unserializing the PHP data when reading this response object from
+   * cache, which can be very costly, and is unnecessary.
+   *
+   * @param \Drupal\jsonapi\ResourceResponse $response
+   *   A fully rendered resource response.
+   *
+   * @return \Drupal\Core\Cache\CacheableResponse|\Symfony\Component\HttpFoundation\Response
+   *   The flattened response.
+   */
+  protected function flattenResponse(ResourceResponse $response) {
+    $final_response = ($response instanceof CacheableResponseInterface) ? new CacheableResponse() : new Response();
+    $final_response->setContent($response->getContent());
+    $final_response->setStatusCode($response->getStatusCode());
+    $final_response->setProtocolVersion($response->getProtocolVersion());
+    $final_response->setCharset($response->getCharset());
+    $final_response->headers = clone $response->headers;
+    if ($final_response instanceof CacheableResponseInterface) {
+      $final_response->addCacheableDependency($response->getCacheableMetadata());
+    }
+    return $final_response;
+  }
+
+  /**
+   * Validates a response against the JSON API specification.
+   *
+   * @param \Symfony\Component\HttpFoundation\Response $response
+   *   The response to validate.
+   *
+   * @return bool
+   *   FALSE if the response failed validation, otherwise TRUE.
+   */
+  protected function validateResponse(Response $response) {
+    if (!class_exists("\\JsonSchema\\Validator")) {
+      return TRUE;
+    }
+    // Do not use Json::decode here since it coerces the response into an
+    // associative array, which creates validation errors.
+    $response_data = json_decode($response->getContent());
+    if (empty($response_data)) {
+      return TRUE;
+    }
+
+    $validator = new Validator();
+    $schema_path = dirname(dirname(__DIR__)) . '/schema.json';
+
+    $validator->check($response_data, (object) ['$ref' => 'file://' . $schema_path]);
+
+    if (!$validator->isValid()) {
+      $this->logger->debug('Response failed validation: @data', [
+        '@data' => Json::encode($response_data),
+      ]);
+      $this->logger->debug('Validation errors: @errors', [
+        '@errors' => Json::encode($validator->getErrors()),
+      ]);
+    }
+
+    return $validator->isValid();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    // Run before \Drupal\dynamic_page_cache\EventSubscriber\DynamicPageCacheSubscriber.
+    $events[KernelEvents::RESPONSE][] = ['onResponse', 110];
+    return $events;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Exception/EntityAccessDeniedHttpException.php b/drupal/modules/jsonapi/src/Exception/EntityAccessDeniedHttpException.php
new file mode 100644
index 0000000..514f732
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Exception/EntityAccessDeniedHttpException.php
@@ -0,0 +1,72 @@
+<?php
+
+namespace Drupal\jsonapi\Exception;
+
+use Drupal\Core\Access\AccessResultInterface;
+use Drupal\Core\Access\AccessResultReasonInterface;
+use Drupal\Core\DependencyInjection\DependencySerializationTrait;
+use Drupal\Core\Entity\EntityInterface;
+use Symfony\Component\HttpKernel\Exception\HttpException;
+
+/**
+ * Enhances the access denied exception with information about the entity.
+ */
+class EntityAccessDeniedHttpException extends HttpException {
+
+  use DependencySerializationTrait;
+
+  /**
+   * The error which caused the 403.
+   *
+   * The error contains:
+   *   - entity: The entity which the current user doens't have access to.
+   *   - pointer: A path in the JSON API response structure pointing to the
+   *     entity.
+   *   - reason: (Optional) An optional reason for this failure.
+   *
+   * @var array
+   */
+  protected $error = [];
+
+  /**
+   * EntityAccessDeniedHttpException constructor.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity.
+   * @param \Drupal\Core\Access\AccessResultInterface $entity_access
+   *   The access result.
+   * @param string $pointer
+   *   (optional) The pointer.
+   * @param string $messsage
+   *   (Optional) The display to display.
+   * @param \Exception|null $previous
+   *   The previous exception.
+   * @param array $headers
+   *   The headers.
+   * @param int $code
+   *   The code.
+   */
+  public function __construct(EntityInterface $entity, AccessResultInterface $entity_access, $pointer, $messsage = 'The current user is not allowed to GET the selected resource.', \Exception $previous = NULL, array $headers = [], $code = 0) {
+    parent::__construct(403, $messsage, $previous, $headers, $code);
+
+    $error = [
+      'entity' => $entity,
+      'pointer' => $pointer,
+      'reason' => NULL,
+    ];
+    if ($entity_access instanceof AccessResultReasonInterface) {
+      $error['reason'] = $entity_access->getReason();
+    }
+    $this->error = $error;
+  }
+
+  /**
+   * Returns the error.
+   *
+   * @return array
+   */
+  public function getError() {
+    return $this->error;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Exception/UnprocessableHttpEntityException.php b/drupal/modules/jsonapi/src/Exception/UnprocessableHttpEntityException.php
new file mode 100644
index 0000000..495f974
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Exception/UnprocessableHttpEntityException.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Drupal\jsonapi\Exception;
+
+use Drupal\Core\Entity\EntityConstraintViolationListInterface;
+use Drupal\Core\DependencyInjection\DependencySerializationTrait;
+use Symfony\Component\HttpKernel\Exception\HttpException;
+
+/**
+ * A class to represent a 422 - Unprocessable Entity Exception.
+ *
+ * The HTTP 422 status code is used when the server sees:-
+ *
+ *  The content type of the request is correct.
+ *  The syntax of the request is correct.
+ *  BUT was unable to process the contained instruction.
+ *
+ * @internal
+ */
+class UnprocessableHttpEntityException extends HttpException {
+
+  use DependencySerializationTrait;
+
+  /**
+   * The constraint violations associated with this exception.
+   *
+   * @var \Drupal\Core\Entity\EntityConstraintViolationListInterface
+   */
+  protected $violations;
+
+  /**
+   * UnprocessableHttpEntityException constructor.
+   *
+   * @param \Exception|null $previous
+   *   The pervious error, if any, associated with the request.
+   * @param array $headers
+   *   The headers associated with the request.
+   * @param int $code
+   *   The HTTP status code associated with the request. Defaults to zero.
+   */
+  public function __construct(\Exception $previous = NULL, array $headers = [], $code = 0) {
+    parent::__construct(422, "Unprocessable Entity: validation failed.", $previous, $headers, $code);
+  }
+
+  /**
+   * Gets the constraint violations associated with this exception.
+   *
+   * @return \Drupal\Core\Entity\EntityConstraintViolationListInterface
+   *   The constraint violations.
+   */
+  public function getViolations() {
+    return $this->violations;
+  }
+
+  /**
+   * Sets the constraint violations associated with this exception.
+   *
+   * @param \Drupal\Core\Entity\EntityConstraintViolationListInterface $violations
+   *   The constraint violations.
+   */
+  public function setViolations(EntityConstraintViolationListInterface $violations) {
+    $this->violations = $violations;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Field/FileDownloadUrl.php b/drupal/modules/jsonapi/src/Field/FileDownloadUrl.php
new file mode 100644
index 0000000..c797871
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Field/FileDownloadUrl.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace Drupal\jsonapi\Field;
+
+use Drupal\Core\Field\FieldItemList;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * @internal
+ */
+class FileDownloadUrl extends FieldItemList {
+
+  /**
+   * Creates a relative URL out of a URI.
+   *
+   * This is a wrapper to the procedural code for testing purposes. For obvious
+   * reasons this method will not be unit tested, but that is fine since it's
+   * only using already tested Drupal API functions.
+   *
+   * @param string $uri
+   *   The URI to transform.
+   *
+   * @return string
+   *   The transformed relative URL.
+   */
+  protected function fileCreateRootRelativeUrl($uri) {
+    return file_url_transform_relative(file_create_url($uri));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getValue($include_computed = FALSE) {
+    $this->initList();
+
+    return parent::getValue($include_computed);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access($operation = 'view', AccountInterface $account = NULL, $return_as_object = FALSE) {
+    return $this->getEntity()
+      ->get('uri')
+      ->access($operation, $account, $return_as_object);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isEmpty() {
+    return $this->getEntity()->get('uri')->isEmpty();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getIterator() {
+    $this->initList();
+
+    return parent::getIterator();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function get($index) {
+    $this->initList();
+
+    return parent::get($index);
+  }
+
+  /**
+   * Initialize the internal field list with the modified items.
+   */
+  protected function initList() {
+    if ($this->list) {
+      return;
+    }
+    $url_list = [];
+    foreach ($this->getEntity()->get('uri') as $uri_item) {
+      $url_item = clone $uri_item;
+      $uri = $uri_item->value;
+      $url_item->setValue($this->fileCreateRootRelativeUrl($uri));
+      $url_list[] = $url_item;
+    }
+    $this->list = $url_list;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/JsonApiSpec.php b/drupal/modules/jsonapi/src/JsonApiSpec.php
new file mode 100644
index 0000000..f6ab332
--- /dev/null
+++ b/drupal/modules/jsonapi/src/JsonApiSpec.php
@@ -0,0 +1,117 @@
+<?php
+
+namespace Drupal\jsonapi;
+
+/**
+ * Defines constants used for compliance with the JSON API specification.
+ *
+ * @see http://jsonapi.org/format
+ *
+ * @internal
+ */
+class JsonApiSpec {
+
+  /**
+   * Member name: globally allowed characters.
+   *
+   * U+0080 and above (non-ASCII Unicode characters) are allowed, but are not
+   * URL-safe. It is RECOMMENDED to not use them.
+   *
+   * A character class, for use in regular expressions.
+   *
+   * @see http://jsonapi.org/format/#document-member-names-allowed-characters
+   * @see http://php.net/manual/en/regexp.reference.character-classes.php
+   */
+  const MEMBER_NAME_GLOBALLY_ALLOWED_CHARACTER_CLASS = '[a-zA-Z0-9\x{80}-\x{10FFFF}]';
+
+  /**
+   * Member name: allowed characters except as the first or last character.
+   *
+   * Space (U+0020) is allowed, but is not URL-safe. It is RECOMMENDED to not
+   * use it.
+   *
+   * A character class, for use in regular expressions.
+   *
+   * @see http://jsonapi.org/format/#document-member-names-allowed-characters
+   * @see http://php.net/manual/en/regexp.reference.character-classes.php
+   */
+  const MEMBER_NAME_INNER_ALLOWED_CHARACTERS = "[a-zA-Z0-9\x{80}-\x{10FFFF}\-_ ]";
+
+  /**
+   * Checks whether the given member name is valid.
+   *
+   * Requirements:
+   * - it MUST contain at least one character.
+   * - it MUST contain only the allowed characters
+   * - it MUST start and end with a "globally allowed character"
+   *
+   * @param string $member_name
+   *   A member name to validate.
+   *
+   * @return bool
+   *
+   * @see http://jsonapi.org/format/#document-member-names
+   */
+  public static function isValidMemberName($member_name) {
+    // @todo When D8 requires PHP >=5.6, move to a MEMBER_NAME_REGEXP constant.
+    static $regexp;
+    if (!isset($regexp)) {
+      $regexp = '/^' .
+        // First character must be "globally allowed". Length must be >=1.
+        self::MEMBER_NAME_GLOBALLY_ALLOWED_CHARACTER_CLASS . '{1}' .
+        '(' .
+          // As many non-globally allowed characters as desired.
+          self::MEMBER_NAME_INNER_ALLOWED_CHARACTERS . '*' .
+          // If lenght is >1, then it must end in a "globally allowed" character.
+          self::MEMBER_NAME_GLOBALLY_ALLOWED_CHARACTER_CLASS . '{1}' .
+        // >1 characters is optional.
+        ')?' .
+        '$/u';
+    }
+
+    return preg_match($regexp, $member_name) === 1;
+  }
+
+  /**
+   * The reserved (official) query parameters.
+   *
+   * @todo When D8 requires PHP >= 5.6, convert to an array.
+   */
+  const RESERVED_QUERY_PARAMETERS = 'filter|sort|page|fields|include';
+
+  /**
+   * Gets the reserved (official) JSON API query parameters.
+   *
+   * @return string[]
+   */
+  public static function getReservedQueryParameters() {
+    return explode('|', static::RESERVED_QUERY_PARAMETERS);
+  }
+
+  /**
+   * Checks whether the given custom query parameter name is valid.
+   *
+   * A custom query parameter name must be a valid member name, with one
+   * additional requirement: it MUST contain at least one non a-z character.
+   *
+   * Requirements:
+   * - it MUST contain at least one character.
+   * - it MUST contain only the allowed characters
+   * - it MUST start and end with a "globally allowed character"
+   * - it MUST contain at least none a-z (U+0061 to U+007A) character
+   *
+   * It is RECOMMENDED that a hyphen (U+002D), underscore (U+005F) or capital
+   * letter is used (i.e. camelCasing).
+   *
+   * @param string $custom_query_parameter_name
+   *   A custom query parameter name to validate.
+   *
+   * @return bool
+   *
+   * @see http://jsonapi.org/format/#query-parameters
+   */
+  public static function isValidCustomQueryParameter($custom_query_parameter_name) {
+    return static::isValidMemberName($custom_query_parameter_name) && preg_match('/[^a-z]/u', $custom_query_parameter_name) === 1;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/JsonapiServiceProvider.php b/drupal/modules/jsonapi/src/JsonapiServiceProvider.php
new file mode 100644
index 0000000..f801059
--- /dev/null
+++ b/drupal/modules/jsonapi/src/JsonapiServiceProvider.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Drupal\jsonapi;
+
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\DependencyInjection\ServiceModifierInterface;
+use Drupal\Core\DependencyInjection\ServiceProviderInterface;
+use Drupal\jsonapi\DependencyInjection\Compiler\RemoveJsonapiFormatCompilerPass;
+use Symfony\Component\DependencyInjection\Compiler\PassConfig;
+
+/**
+ * Adds 'api_json' as known format and prevents its use in the REST module.
+ *
+ * @internal
+ */
+class JsonapiServiceProvider implements ServiceModifierInterface, ServiceProviderInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function alter(ContainerBuilder $container) {
+    if ($container->has('http_middleware.negotiation') && is_a($container->getDefinition('http_middleware.negotiation')
+      ->getClass(), '\Drupal\Core\StackMiddleware\NegotiationMiddleware', TRUE)
+    ) {
+      // @see http://www.iana.org/assignments/media-types/application/vnd.api+json
+      $container->getDefinition('http_middleware.negotiation')
+        ->addMethodCall('registerFormat', [
+          'api_json',
+          ['application/vnd.api+json'],
+        ]);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function register(ContainerBuilder $container) {
+    $container->addCompilerPass(new RemoveJsonapiFormatCompilerPass(), PassConfig::TYPE_REMOVE);
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/LinkManager/LinkManager.php b/drupal/modules/jsonapi/src/LinkManager/LinkManager.php
new file mode 100644
index 0000000..b6e6328
--- /dev/null
+++ b/drupal/modules/jsonapi/src/LinkManager/LinkManager.php
@@ -0,0 +1,205 @@
+<?php
+
+namespace Drupal\jsonapi\LinkManager;
+
+use Drupal\Core\Routing\UrlGeneratorInterface;
+use Drupal\jsonapi\ResourceType\ResourceType;
+use Drupal\jsonapi\Routing\Param\OffsetPage;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
+
+/**
+ * @internal
+ */
+class LinkManager {
+
+  /**
+   * @var \Symfony\Component\Routing\Matcher\RequestMatcherInterface
+   */
+  protected $router;
+
+  /**
+   * @var \Drupal\Core\Render\MetadataBubblingUrlGenerator
+   */
+  protected $urlGenerator;
+
+  /**
+   * Instantiates a LinkManager object.
+   *
+   * @param \Symfony\Component\Routing\Matcher\RequestMatcherInterface $router
+   *   The router.
+   * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
+   *   The Url generator.
+   */
+  public function __construct(RequestMatcherInterface $router, UrlGeneratorInterface $url_generator) {
+    $this->router = $router;
+    $this->urlGenerator = $url_generator;
+  }
+
+  /**
+   * Gets a link for the entity.
+   *
+   * @param int $entity_id
+   *   The entity ID to generate the link for. Note: Depending on the
+   *   configuration this might be the UUID as well.
+   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
+   *   The JSON API resource type.
+   * @param array $route_parameters
+   *   Parameters for the route generation.
+   * @param string $key
+   *   A key to build the route identifier.
+   *
+   * @return string
+   *   The URL string.
+   */
+  public function getEntityLink($entity_id, ResourceType $resource_type, array $route_parameters, $key) {
+    $route_parameters += [
+      $resource_type->getEntityTypeId() => $entity_id,
+    ];
+    $route_key = sprintf('jsonapi.%s.%s', $resource_type->getTypeName(), $key);
+    return $this->urlGenerator->generateFromRoute($route_key, $route_parameters, ['absolute' => TRUE]);
+  }
+
+  /**
+   * Get the full URL for a given request object.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object.
+   * @param array|null $query
+   *   The query parameters to use. Leave it empty to get the query from the
+   *   request object.
+   *
+   * @return string
+   *   The full URL.
+   */
+  public function getRequestLink(Request $request, $query = NULL) {
+    $query = $query ?: (array) $request->query->getIterator();
+    $result = $this->router->matchRequest($request);
+    $route_name = $result[RouteObjectInterface::ROUTE_NAME];
+    /* @var \Symfony\Component\HttpFoundation\ParameterBag $raw_variables */
+    $raw_variables = $result['_raw_variables'];
+    $route_parameters = $raw_variables->all();
+    $options = [
+      'absolute' => TRUE,
+      'query' => $query,
+    ];
+    return $this->urlGenerator->generateFromRoute($route_name, $route_parameters, $options);
+  }
+
+  /**
+   * Get the pager links for a given request object.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object.
+   * @param array $link_context
+   *   An associative array with extra data to build the links.
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
+   *   When the offset and size are invalid.
+   *
+   * @return string[]
+   *   An array of URLs, with:
+   *   - a 'next' key if it is not the last page;
+   *   - 'prev' and 'first' keys if it's not the first page.
+   */
+  public function getPagerLinks(Request $request, array $link_context = []) {
+    if (!empty($link_context['total_count']) && !$total = (int) $link_context['total_count']) {
+      return [];
+    }
+    $params = $request->get('_json_api_params');
+    if ($page_param = $params[OffsetPage::KEY_NAME]) {
+      /* @var \Drupal\jsonapi\Routing\Param\OffsetPage $page_param */
+      $offset = $page_param->getOffset();
+      $size = $page_param->getSize();
+    }
+    else {
+      // Apply the defaults.
+      $offset = 0;
+      $size = OffsetPage::$maxSize;
+    }
+    if ($size <= 0) {
+      throw new BadRequestHttpException(sprintf('The page size needs to be a positive integer.'));
+    }
+    $query = (array) $request->query->getIterator();
+    $links = [];
+    // Check if this is not the last page.
+    if ($link_context['has_next_page']) {
+      $links['next'] = $this->getRequestLink($request, $this->getPagerQueries('next', $offset, $size, $query));
+
+      if (!empty($total)) {
+        $links['last'] = $this->getRequestLink($request, $this->getPagerQueries('last', $offset, $size, $query, $total));
+      }
+    }
+    // Check if this is not the first page.
+    if ($offset > 0) {
+      $links['first'] = $this->getRequestLink($request, $this->getPagerQueries('first', $offset, $size, $query));
+      $links['prev'] = $this->getRequestLink($request, $this->getPagerQueries('prev', $offset, $size, $query));
+    }
+
+    return $links;
+  }
+
+  /**
+   * Get the query param array.
+   *
+   * @param string $link_id
+   *   The name of the pagination link requested.
+   * @param int $offset
+   *   The starting index.
+   * @param int $size
+   *   The pagination page size.
+   * @param array $query
+   *   The query parameters.
+   * @param int $total
+   *   The total size of the collection.
+   *
+   * @return array
+   *   The pagination query param array.
+   */
+  protected function getPagerQueries($link_id, $offset, $size, array $query = [], $total = 0) {
+    $extra_query = [];
+    switch ($link_id) {
+      case 'next':
+        $extra_query = [
+          'page' => [
+            'offset' => $offset + $size,
+            'limit' => $size,
+          ],
+        ];
+        break;
+
+      case 'first':
+        $extra_query = [
+          'page' => [
+            'offset' => 0,
+            'limit' => $size,
+          ],
+        ];
+        break;
+
+      case 'last':
+        if ($total) {
+          $extra_query = [
+            'page' => [
+              'offset' => (ceil($total / $size) - 1) * $size,
+              'limit' => $size,
+            ],
+          ];
+        }
+        break;
+
+      case 'prev':
+        $extra_query = [
+          'page' => [
+            'offset' => max($offset - $size, 0),
+            'limit' => $size,
+          ],
+        ];
+        break;
+    }
+    return array_merge($query, $extra_query);
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Normalizer/ConfigEntityNormalizer.php b/drupal/modules/jsonapi/src/Normalizer/ConfigEntityNormalizer.php
new file mode 100644
index 0000000..5d97676
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Normalizer/ConfigEntityNormalizer.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace Drupal\jsonapi\Normalizer;
+
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
+use Drupal\jsonapi\Normalizer\Value\FieldItemNormalizerValue;
+use Drupal\jsonapi\Normalizer\Value\FieldNormalizerValue;
+use Drupal\jsonapi\ResourceType\ResourceType;
+
+/**
+ * Converts the Drupal config entity object to a JSON API array structure.
+ */
+class ConfigEntityNormalizer extends EntityNormalizer {
+
+  /**
+   * The interface or class that this Normalizer supports.
+   *
+   * @var string
+   */
+  protected $supportedInterfaceOrClass = ConfigEntityInterface::class;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getFields($entity, $bundle, ResourceType $resource_type) {
+    $enabled_public_fields = [];
+    $fields = $entity->toArray();
+    // Filter the array based on the field names.
+    $enabled_field_names = array_filter(
+      array_keys($fields),
+      [$resource_type, 'isFieldEnabled']
+    );
+    // Return a sub-array of $output containing the keys in $enabled_fields.
+    $input = array_intersect_key($fields, array_flip($enabled_field_names));
+    /* @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */
+    foreach ($input as $field_name => $field_value) {
+      $public_field_name = $resource_type->getPublicName($field_name);
+      $enabled_public_fields[$public_field_name] = $field_value;
+    }
+    return $enabled_public_fields;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function serializeField($field, array $context, $format) {
+    $output = $this->serializer->normalize($field, $format, $context);
+    if (is_array($output)) {
+      $output = new FieldNormalizerValue(
+        [new FieldItemNormalizerValue($output)],
+        1
+      );
+      $output->setPropertyType('attributes');
+      return $output;
+    }
+    $field instanceof Relationship ?
+      $output->setPropertyType('relationships') :
+      $output->setPropertyType('attributes');
+    return $output;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Normalizer/ContentEntityNormalizer.php b/drupal/modules/jsonapi/src/Normalizer/ContentEntityNormalizer.php
new file mode 100644
index 0000000..6f8fcf0
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Normalizer/ContentEntityNormalizer.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace Drupal\jsonapi\Normalizer;
+
+/**
+ * Converts the Drupal content entity object to a JSON API array structure.
+ */
+class ContentEntityNormalizer extends EntityNormalizer {}
diff --git a/drupal/modules/jsonapi/src/Normalizer/EntityAccessDeniedHttpExceptionNormalizer.php b/drupal/modules/jsonapi/src/Normalizer/EntityAccessDeniedHttpExceptionNormalizer.php
new file mode 100644
index 0000000..783cc62
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Normalizer/EntityAccessDeniedHttpExceptionNormalizer.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Drupal\jsonapi\Normalizer;
+
+use Drupal\jsonapi\Exception\EntityAccessDeniedHttpException;
+use Symfony\Component\HttpKernel\Exception\HttpException;
+
+/**
+ * Normalizes an EntityAccessDeniedException object for JSON output which
+ * complies with the JSON API specification. A source pointer is added to help
+ * client applications report to know which entity was access denied.
+ *
+ * @see http://jsonapi.org/format/#error-objects
+ */
+class EntityAccessDeniedHttpExceptionNormalizer extends HttpExceptionNormalizer {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $supportedInterfaceOrClass = EntityAccessDeniedHttpException::class;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function buildErrorObjects(HttpException $exception) {
+    $errors = parent::buildErrorObjects($exception);
+
+    if ($exception instanceof EntityAccessDeniedHttpException) {
+      $error = $exception->getError();
+      /** @var \Drupal\Core\Entity\EntityInterface $entity */
+      $entity = $error['entity'];
+      $pointer = $error['pointer'];
+      $reason = $error['reason'];
+
+      $errors[0]['id'] = sprintf(
+        '/%s--%s/%s',
+        $entity->getEntityTypeId(),
+        $entity->bundle(),
+        $entity->uuid()
+      );
+      $errors[0]['source']['pointer'] = $pointer;
+
+      if ($reason) {
+        $errors[0]['detail'] = isset($errors[0]['detail']) ? $errors[0]['detail'] . ' ' . $reason : $reason;
+      }
+    }
+
+    return $errors;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Normalizer/EntityNormalizer.php b/drupal/modules/jsonapi/src/Normalizer/EntityNormalizer.php
new file mode 100644
index 0000000..8d42092
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Normalizer/EntityNormalizer.php
@@ -0,0 +1,260 @@
+<?php
+
+namespace Drupal\jsonapi\Normalizer;
+
+use Drupal\Core\Access\AccessibleInterface;
+use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Field\EntityReferenceFieldItemList;
+use Drupal\jsonapi\Normalizer\Value\EntityNormalizerValue;
+use Drupal\jsonapi\ResourceType\ResourceType;
+use Drupal\jsonapi\LinkManager\LinkManager;
+use Drupal\jsonapi\Normalizer\Value\NullFieldNormalizerValue;
+use Drupal\jsonapi\ResourceType\ResourceTypeRepository;
+use Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException;
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
+
+/**
+ * Converts the Drupal entity object to a JSON API array structure.
+ */
+class EntityNormalizer extends NormalizerBase implements DenormalizerInterface {
+
+  /**
+   * The interface or class that this Normalizer supports.
+   *
+   * @var string
+   */
+  protected $supportedInterfaceOrClass = ContentEntityInterface::class;
+
+  /**
+   * The formats that the Normalizer can handle.
+   *
+   * @var array
+   */
+  protected $formats = ['api_json'];
+
+  /**
+   * The link manager.
+   *
+   * @var \Drupal\jsonapi\LinkManager\LinkManager
+   */
+  protected $linkManager;
+
+  /**
+   * The JSON API resource type repository.
+   *
+   * @var \Drupal\jsonapi\ResourceType\ResourceTypeRepository
+   */
+  protected $resourceTypeRepository;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructs an EntityNormalizer object.
+   *
+   * @param \Drupal\jsonapi\LinkManager\LinkManager $link_manager
+   *   The link manager.
+   * @param \Drupal\jsonapi\ResourceType\ResourceTypeRepository $resource_type_repository
+   *   The JSON API resource type repository.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   */
+  public function __construct(LinkManager $link_manager, ResourceTypeRepository $resource_type_repository, EntityTypeManagerInterface $entity_type_manager) {
+    $this->linkManager = $link_manager;
+    $this->resourceTypeRepository = $resource_type_repository;
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function normalize($entity, $format = NULL, array $context = []) {
+    // If the fields to use were specified, only output those field values.
+    $context['resource_type'] = $resource_type = $this->resourceTypeRepository->get(
+      $entity->getEntityTypeId(),
+      $entity->bundle()
+    );
+    $resource_type_name = $resource_type->getTypeName();
+    // Get the bundle ID of the requested resource. This is used to determine if
+    // this is a bundle level resource or an entity level resource.
+    $bundle = $resource_type->getBundle();
+    if (!empty($context['sparse_fieldset'][$resource_type_name])) {
+      $field_names = $context['sparse_fieldset'][$resource_type_name];
+    }
+    else {
+      $field_names = $this->getFieldNames($entity, $bundle, $resource_type);
+    }
+    /* @var Value\FieldNormalizerValueInterface[] $normalizer_values */
+    $normalizer_values = [];
+    foreach ($this->getFields($entity, $bundle, $resource_type) as $field_name => $field) {
+      if (!in_array($field_name, $field_names)) {
+        continue;
+      }
+      $normalizer_values[$field_name] = $this->serializeField($field, $context, $format);
+    }
+
+    $link_context = ['link_manager' => $this->linkManager];
+    $output = new EntityNormalizerValue($normalizer_values, $context, $entity, $link_context);
+    // Add the entity level cacheability metadata.
+    $output->addCacheableDependency($entity);
+    $output->addCacheableDependency($output);
+    // Add the field level cacheability metadata.
+    array_walk($normalizer_values, function ($normalizer_value) {
+      if ($normalizer_value instanceof RefinableCacheableDependencyInterface) {
+        $normalizer_value->addCacheableDependency($normalizer_value);
+      }
+    });
+    return $output;
+  }
+
+  /**
+   * Checks if the passed field is a relationship field.
+   *
+   * @param mixed $field
+   *   The field.
+   *
+   * @return bool
+   *   TRUE if it's a JSON API relationship.
+   */
+  protected function isRelationship($field) {
+    return $field instanceof EntityReferenceFieldItemList || $field instanceof Relationship;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function denormalize($data, $class, $format = NULL, array $context = []) {
+    if (empty($context['resource_type']) || !$context['resource_type'] instanceof ResourceType) {
+      throw new PreconditionFailedHttpException('Missing context during denormalization.');
+    }
+    /* @var \Drupal\jsonapi\ResourceType\ResourceType $resource_type */
+    $resource_type = $context['resource_type'];
+    $entity_type_id = $resource_type->getEntityTypeId();
+    $bundle = $resource_type->getBundle();
+    $bundle_key = $this->entityTypeManager->getDefinition($entity_type_id)
+      ->getKey('bundle');
+    if ($bundle_key && $bundle) {
+      $data[$bundle_key] = $bundle;
+    }
+
+    return $this->entityTypeManager->getStorage($entity_type_id)
+      ->create($this->prepareInput($data, $resource_type));
+  }
+
+  /**
+   * Gets the field names for the given entity.
+   *
+   * @param mixed $entity
+   *   The entity.
+   * @param string $bundle
+   *   The entity bundle.
+   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
+   *   The resource type.
+   *
+   * @return string[]
+   *   The field names.
+   */
+  protected function getFieldNames($entity, $bundle, ResourceType $resource_type) {
+    /* @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+    return array_keys($this->getFields($entity, $bundle, $resource_type));
+  }
+
+  /**
+   * Gets the field names for the given entity.
+   *
+   * @param mixed $entity
+   *   The entity.
+   * @param string $bundle
+   *   The bundle id.
+   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
+   *   The resource type.
+   *
+   * @return array
+   *   The fields.
+   */
+  protected function getFields($entity, $bundle, ResourceType $resource_type) {
+    $output = [];
+    $fields = $entity->getFields();
+    // Filter the array based on the field names.
+    $enabled_field_names = array_filter(
+      array_keys($fields),
+      [$resource_type, 'isFieldEnabled']
+    );
+    // Return a sub-array of $output containing the keys in $enabled_fields.
+    $input = array_intersect_key($fields, array_flip($enabled_field_names));
+    /* @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+    foreach ($input as $field_name => $field_value) {
+      $public_field_name = $resource_type->getPublicName($field_name);
+      $output[$public_field_name] = $field_value;
+    }
+    return $output;
+  }
+
+  /**
+   * Serializes a given field.
+   *
+   * @param mixed $field
+   *   The field to serialize.
+   * @param array $context
+   *   The normalization context.
+   * @param string $format
+   *   The serialization format.
+   *
+   * @return Value\FieldNormalizerValueInterface
+   *   The normalized value.
+   */
+  protected function serializeField($field, array $context, $format) {
+    /* @var \Drupal\Core\Field\FieldItemListInterface|\Drupal\jsonapi\Normalizer\Relationship $field */
+    // Continue if the current user does not have access to view this field.
+    $access = $field->access('view', $context['account'], TRUE);
+    if ($field instanceof AccessibleInterface && !$access->isAllowed()) {
+      return (new NullFieldNormalizerValue())->addCacheableDependency($access);
+    }
+    /** @var \Drupal\jsonapi\Normalizer\Value\FieldNormalizerValue $output */
+    $output = $this->serializer->normalize($field, $format, $context);
+    $is_relationship = $this->isRelationship($field);
+    $property_type = $is_relationship ? 'relationships' : 'attributes';
+    $output->setPropertyType($property_type);
+
+    if ($output instanceof RefinableCacheableDependencyInterface) {
+      // Add the cache dependency to the field level object because we want to
+      // allow the field normalizers to add extra cacheability metadata.
+      $output->addCacheableDependency($access);
+    }
+
+    return $output;
+  }
+
+  /**
+   * Prepares the input data to create the entity.
+   *
+   * @param array $data
+   *   The input data to modify.
+   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
+   *   Contains the info about the resource type.
+   *
+   * @return array
+   *   The modified input data.
+   */
+  protected function prepareInput(array $data, ResourceType $resource_type) {
+    $data_internal = [];
+    // Translate the public fields into the entity fields.
+    foreach ($data as $public_field_name => $field_value) {
+      // Skip any disabled field.
+      if (!$resource_type->isFieldEnabled($public_field_name)) {
+        continue;
+      }
+      $internal_name = $resource_type->getInternalName($public_field_name);
+      $data_internal[$internal_name] = $field_value;
+    }
+
+    return $data_internal;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Normalizer/EntityReferenceFieldNormalizer.php b/drupal/modules/jsonapi/src/Normalizer/EntityReferenceFieldNormalizer.php
new file mode 100644
index 0000000..d32abe9
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Normalizer/EntityReferenceFieldNormalizer.php
@@ -0,0 +1,222 @@
+<?php
+
+namespace Drupal\jsonapi\Normalizer;
+
+use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Entity\EntityRepositoryInterface;
+use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
+use Drupal\Core\Field\FieldTypePluginManagerInterface;
+use Drupal\Core\Field\TypedData\FieldItemDataDefinition;
+use Drupal\jsonapi\ResourceType\ResourceTypeRepository;
+use Drupal\jsonapi\Resource\EntityCollection;
+use Drupal\jsonapi\LinkManager\LinkManager;
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+
+/**
+ * Normalizer class specific for entity reference field objects.
+ */
+class EntityReferenceFieldNormalizer extends FieldNormalizer implements DenormalizerInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $supportedInterfaceOrClass = EntityReferenceFieldItemListInterface::class;
+
+  /**
+   * The link manager.
+   *
+   * @param \Drupal\jsonapi\LinkManager\LinkManager
+   */
+  protected $linkManager;
+
+  /**
+   * The entity field manager.
+   *
+   * @param \Drupal\Core\Entity\EntityFieldManagerInterface
+   */
+  protected $fieldManager;
+
+  /**
+   * The field plugin manager.
+   *
+   * @param \Drupal\Core\Field\FieldTypePluginManagerInterface
+   */
+  protected $pluginManager;
+
+  /**
+   * The entity repository.
+   *
+   * @var \Drupal\Core\Entity\EntityRepositoryInterface
+   */
+  protected $entityRepository;
+
+  /**
+   * Instantiates a EntityReferenceFieldNormalizer object.
+   *
+   * @param \Drupal\jsonapi\LinkManager\LinkManager $link_manager
+   *   The link manager.
+   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $field_manager
+   *   The entity field manager.
+   * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $plugin_manager
+   *   The plugin manager for fields.
+   * @param \Drupal\jsonapi\ResourceType\ResourceTypeRepository $resource_type_repository
+   *   The JSON API resource type repository.
+   * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
+   *   The entity repository.
+   */
+  public function __construct(LinkManager $link_manager, EntityFieldManagerInterface $field_manager, FieldTypePluginManagerInterface $plugin_manager, ResourceTypeRepository $resource_type_repository, EntityRepositoryInterface $entity_repository) {
+    $this->linkManager = $link_manager;
+    $this->fieldManager = $field_manager;
+    $this->pluginManager = $plugin_manager;
+    $this->resourceTypeRepository = $resource_type_repository;
+    $this->entityRepository = $entity_repository;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function normalize($field, $format = NULL, array $context = []) {
+    /* @var $field \Drupal\Core\Field\FieldItemListInterface */
+    // Build the relationship object based on the Entity Reference and normalize
+    // that object instead.
+    $main_property = $field->getItemDefinition()->getMainPropertyName();
+    $definition = $field->getFieldDefinition();
+    $cardinality = $definition
+      ->getFieldStorageDefinition()
+      ->getCardinality();
+    /** @var \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem[] $entity_reference_item_list */
+    $entity_reference_item_list = array_filter(iterator_to_array($field), function ($item) {
+      return (bool) $item->get('entity')->getValue();
+    });
+    $entity_list_metadata = [];
+    $entity_list = [];
+    foreach ($entity_reference_item_list as $item) {
+      // Prepare a list of additional properties stored by the field.
+      $metadata = [];
+      /** @var \Drupal\Core\TypedData\TypedDataInterface[] $properties */
+      $properties = $item->getProperties();
+      foreach ($properties as $property_key => $property) {
+        if ($property_key !== $main_property) {
+          $metadata[$property_key] = $property->getValue();
+        }
+      }
+      $entity_list_metadata[] = $metadata;
+
+      // Get the referenced entity.
+      $entity = $item->get('entity')->getValue();
+      // And get the translation in the requested language.
+      $entity_list[] = $this->entityRepository->getTranslationFromContext($entity);
+    }
+    $entity_collection = new EntityCollection($entity_list);
+    $relationship = new Relationship($this->resourceTypeRepository, $field->getName(), $cardinality, $entity_collection, $field->getEntity(), $main_property, $entity_list_metadata);
+    return $this->serializer->normalize($relationship, $format, $context);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function denormalize($data, $class, $format = NULL, array $context = []) {
+    // If we get to here is through a write method on a relationship operation.
+    /** @var \Drupal\jsonapi\ResourceType\ResourceType $resource_type */
+    $resource_type = $context['resource_type'];
+    $entity_type_id = $resource_type->getEntityTypeId();
+    $field_definitions = $this->fieldManager->getFieldDefinitions(
+      $entity_type_id,
+      $resource_type->getBundle()
+    );
+    if (empty($context['related']) || empty($field_definitions[$context['related']])) {
+      throw new BadRequestHttpException('Invalid or missing related field.');
+    }
+    /* @var \Drupal\field\Entity\FieldConfig $field_definition */
+    $field_definition = $field_definitions[$context['related']];
+    // This is typically 'target_id'.
+    $item_definition = $field_definition->getItemDefinition();
+    $property_key = $item_definition->getMainPropertyName();
+    $target_resources = $this->getAllowedResourceTypes($item_definition);
+
+    $is_multiple = $field_definition->getFieldStorageDefinition()->isMultiple();
+    $data = $this->massageRelationshipInput($data, $is_multiple);
+    $values = array_map(function ($value) use ($property_key, $target_resources) {
+      // Make sure that the provided type is compatible with the targeted
+      // resource.
+      if (!in_array($value['type'], $target_resources)) {
+        throw new BadRequestHttpException(sprintf(
+          'The provided type (%s) does not mach the destination resource types (%s).',
+          $value['type'],
+          implode(', ', $target_resources)
+        ));
+      }
+
+      // Load the entity by UUID.
+      list($entity_type_id,) = explode('--', $value['type']);
+      $entity = $this->entityRepository->loadEntityByUuid($entity_type_id, $value['id']);
+      $value['id'] = $entity ? $entity->id() : NULL;
+
+      return [$property_key => $value['id']];
+    }, $data['data']);
+    return $this->pluginManager
+      ->createFieldItemList($context['target_entity'], $context['related'], $values);
+  }
+
+  /**
+   * Validates and massages the relationship input depending on the cardinality.
+   *
+   * @param array $data
+   *   The input data from the body.
+   * @param bool $is_multiple
+   *   Indicates if the relationship is to-many.
+   *
+   * @return array
+   *   The massaged data array.
+   */
+  protected function massageRelationshipInput(array $data, $is_multiple) {
+    if ($is_multiple) {
+      if (!is_array($data['data'])) {
+        throw new BadRequestHttpException('Invalid body payload for the relationship.');
+      }
+      // Leave the invalid elements.
+      $invalid_elements = array_filter($data['data'], function ($element) {
+        return empty($element['type']) || empty($element['id']);
+      });
+      if ($invalid_elements) {
+        throw new BadRequestHttpException('Invalid body payload for the relationship.');
+      }
+    }
+    else {
+      // For to-one relationships you can have a NULL value.
+      if (is_null($data['data'])) {
+        return ['data' => []];
+      }
+      if (empty($data['data']['type']) || empty($data['data']['id'])) {
+        throw new BadRequestHttpException('Invalid body payload for the relationship.');
+      }
+      $data['data'] = [$data['data']];
+    }
+    return $data;
+  }
+
+  /**
+   * Build the list of resource types supported by this entity reference field.
+   *
+   * @param \Drupal\Core\Field\TypedData\FieldItemDataDefinition $item_definition
+   *   The field item definition.
+   *
+   * @return string[]
+   *   List of resource types.
+   */
+  protected function getAllowedResourceTypes(FieldItemDataDefinition $item_definition) {
+    // Build the list of allowed resources.
+    $target_entity_id = $item_definition->getSetting('target_type');
+    $handler_settings = $item_definition->getSetting('handler_settings');
+    $target_bundles = empty($handler_settings['target_bundles']) ?
+      [] :
+      $handler_settings['target_bundles'];
+    return array_map(function ($target_bundle) use ($target_entity_id) {
+      return $this->resourceTypeRepository
+        ->get($target_entity_id, $target_bundle)
+        ->getTypeName();
+    }, $target_bundles);
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Normalizer/FieldItemNormalizer.php b/drupal/modules/jsonapi/src/Normalizer/FieldItemNormalizer.php
new file mode 100644
index 0000000..6e86ba8
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Normalizer/FieldItemNormalizer.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Drupal\jsonapi\Normalizer;
+
+use Drupal\Core\Field\FieldItemInterface;
+use Drupal\jsonapi\Normalizer\Value\FieldItemNormalizerValue;
+use Symfony\Component\Serializer\Exception\UnexpectedValueException;
+
+/**
+ * Converts the Drupal field item object to a JSON API array structure.
+ */
+class FieldItemNormalizer extends NormalizerBase {
+
+  /**
+   * The interface or class that this Normalizer supports.
+   *
+   * @var string
+   */
+  protected $supportedInterfaceOrClass = FieldItemInterface::class;
+
+  /**
+   * The formats that the Normalizer can handle.
+   *
+   * @var array
+   */
+  protected $formats = ['api_json'];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function normalize($field_item, $format = NULL, array $context = []) {
+    /** @var \Drupal\Core\TypedData\TypedDataInterface $property */
+    $values = [];
+    // We normalize each individual property, so each can do their own casting,
+    // if needed.
+    foreach ($field_item as $property_name => $property) {
+      $values[$property_name] = $this->serializer->normalize($property, $format, $context);
+    }
+
+    if (isset($context['langcode'])) {
+      $values['lang'] = $context['langcode'];
+    }
+    return new FieldItemNormalizerValue($values);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function denormalize($data, $class, $format = NULL, array $context = []) {
+    throw new UnexpectedValueException('Denormalization not implemented for JSON API');
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Normalizer/FieldNormalizer.php b/drupal/modules/jsonapi/src/Normalizer/FieldNormalizer.php
new file mode 100644
index 0000000..464c65e
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Normalizer/FieldNormalizer.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace Drupal\jsonapi\Normalizer;
+
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\jsonapi\Normalizer\Value\FieldNormalizerValue;
+use Symfony\Component\Serializer\Exception\UnexpectedValueException;
+
+/**
+ * Converts the Drupal field structure to a JSON API array structure.
+ */
+class FieldNormalizer extends NormalizerBase {
+
+  /**
+   * The interface or class that this Normalizer supports.
+   *
+   * @var string
+   */
+  protected $supportedInterfaceOrClass = FieldItemListInterface::class;
+
+  /**
+   * The formats that the Normalizer can handle.
+   *
+   * @var array
+   */
+  protected $formats = ['api_json'];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function normalize($field, $format = NULL, array $context = []) {
+    /* @var \Drupal\Core\Field\FieldItemListInterface $field */
+    return $this->normalizeFieldItems($field, $format, $context);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function denormalize($data, $class, $format = NULL, array $context = []) {
+    throw new UnexpectedValueException('Denormalization not implemented for JSON API');
+  }
+
+  /**
+   * Helper function to normalize field items.
+   *
+   * @param \Drupal\Core\Field\FieldItemListInterface $field
+   *   The field object.
+   * @param string $format
+   *   The format.
+   * @param array $context
+   *   The context array.
+   *
+   * @return \Drupal\jsonapi\Normalizer\Value\FieldNormalizerValue
+   *   The array of normalized field items.
+   */
+  protected function normalizeFieldItems(FieldItemListInterface $field, $format, array $context) {
+    $normalizer_items = [];
+    if (!$field->isEmpty()) {
+      foreach ($field as $field_item) {
+        $normalizer_items[] = $this->serializer->normalize($field_item, $format, $context);
+      }
+    }
+    $cardinality = $field->getFieldDefinition()
+      ->getFieldStorageDefinition()
+      ->getCardinality();
+    return new FieldNormalizerValue($normalizer_items, $cardinality);
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Normalizer/HttpExceptionNormalizer.php b/drupal/modules/jsonapi/src/Normalizer/HttpExceptionNormalizer.php
new file mode 100644
index 0000000..6567da3
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Normalizer/HttpExceptionNormalizer.php
@@ -0,0 +1,153 @@
+<?php
+
+namespace Drupal\jsonapi\Normalizer;
+
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Session\AccountProxyInterface;
+use Drupal\jsonapi\Normalizer\Value\FieldItemNormalizerValue;
+use Drupal\jsonapi\Normalizer\Value\HttpExceptionNormalizerValue;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\HttpException;
+
+/**
+ * Normalizes an HttpException object for JSON output which complies with the
+ * JSON API specification.
+ *
+ * @see http://jsonapi.org/format/#error-objects
+ */
+class HttpExceptionNormalizer extends NormalizerBase {
+
+  /**
+   * The interface or class that this Normalizer supports.
+   *
+   * @var string
+   */
+  protected $supportedInterfaceOrClass = HttpException::class;
+
+  /**
+   * The current user making the request.
+   *
+   * @var \Drupal\Core\Session\AccountProxyInterface
+   */
+  protected $currentUser;
+
+  /**
+   * HttpExceptionNormalizer constructor.
+   *
+   * @param \Drupal\Core\Session\AccountProxyInterface $current_user
+   *   The current user.
+   */
+  public function __construct(AccountProxyInterface $current_user) {
+    $this->currentUser = $current_user;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function normalize($object, $format = NULL, array $context = []) {
+    $errors = $this->buildErrorObjects($object);
+
+    $errors = array_map(function ($error) {
+      return new FieldItemNormalizerValue([$error]);
+    }, $errors);
+
+    return new HttpExceptionNormalizerValue(
+      $errors,
+      FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
+    );
+  }
+
+  /**
+   * Builds the normalized JSON API error objects for the response.
+   *
+   * @param \Symfony\Component\HttpKernel\Exception\HttpException $exception
+   *   The Exception.
+   *
+   * @return array
+   *   The error objects to include in the response.
+   */
+  protected function buildErrorObjects(HttpException $exception) {
+    $error = [];
+    $status_code = $exception->getStatusCode();
+    if (!empty(Response::$statusTexts[$status_code])) {
+      $error['title'] = Response::$statusTexts[$status_code];
+    }
+    $error += [
+      'status' => $status_code,
+      'detail' => $exception->getMessage(),
+      'links' => [
+        'info' => $this->getInfoUrl($status_code),
+      ],
+      'code' => $exception->getCode(),
+    ];
+    if ($this->currentUser->hasPermission('access site reports')) {
+      // The following information may contain sensitive information. Only show
+      // it to authorized users.
+      $error['source'] = [
+        'file' => $exception->getFile(),
+        'line' => $exception->getLine(),
+      ];
+      $error['meta'] = [
+        'exception' => (string) $exception,
+        'trace' => $exception->getTrace(),
+      ];
+    }
+
+    return [$error];
+  }
+
+  /**
+   * Return a string to the common problem type.
+   *
+   * @return string
+   *   URL pointing to the specific RFC-2616 section.
+   */
+  protected function getInfoUrl($status_code) {
+    // Depending on the error code we'll return a different URL.
+    $url = 'http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html';
+    $sections = [
+      '100' => '#sec10.1.1',
+      '101' => '#sec10.1.2',
+      '200' => '#sec10.2.1',
+      '201' => '#sec10.2.2',
+      '202' => '#sec10.2.3',
+      '203' => '#sec10.2.4',
+      '204' => '#sec10.2.5',
+      '205' => '#sec10.2.6',
+      '206' => '#sec10.2.7',
+      '300' => '#sec10.3.1',
+      '301' => '#sec10.3.2',
+      '302' => '#sec10.3.3',
+      '303' => '#sec10.3.4',
+      '304' => '#sec10.3.5',
+      '305' => '#sec10.3.6',
+      '307' => '#sec10.3.8',
+      '400' => '#sec10.4.1',
+      '401' => '#sec10.4.2',
+      '402' => '#sec10.4.3',
+      '403' => '#sec10.4.4',
+      '404' => '#sec10.4.5',
+      '405' => '#sec10.4.6',
+      '406' => '#sec10.4.7',
+      '407' => '#sec10.4.8',
+      '408' => '#sec10.4.9',
+      '409' => '#sec10.4.10',
+      '410' => '#sec10.4.11',
+      '411' => '#sec10.4.12',
+      '412' => '#sec10.4.13',
+      '413' => '#sec10.4.14',
+      '414' => '#sec10.4.15',
+      '415' => '#sec10.4.16',
+      '416' => '#sec10.4.17',
+      '417' => '#sec10.4.18',
+      '500' => '#sec10.5.1',
+      '501' => '#sec10.5.2',
+      '502' => '#sec10.5.3',
+      '503' => '#sec10.5.4',
+      '504' => '#sec10.5.5',
+      '505' => '#sec10.5.6',
+    ];
+    return empty($sections[$status_code]) ? $url : $url . $sections[$status_code];
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php b/drupal/modules/jsonapi/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php
new file mode 100644
index 0000000..f9584d5
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php
@@ -0,0 +1,263 @@
+<?php
+
+namespace Drupal\jsonapi\Normalizer;
+
+use Drupal\Component\Plugin\Exception\PluginNotFoundException;
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
+use Drupal\jsonapi\Context\CurrentContext;
+use Drupal\jsonapi\Normalizer\Value\JsonApiDocumentTopLevelNormalizerValue;
+use Drupal\jsonapi\Resource\EntityCollection;
+use Drupal\jsonapi\LinkManager\LinkManager;
+use Drupal\jsonapi\Resource\JsonApiDocumentTopLevel;
+use Drupal\jsonapi\ResourceType\ResourceType;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
+use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
+use Drupal\jsonapi\ResourceType\ResourceTypeRepository;
+
+/**
+ * @see \Drupal\jsonapi\Resource\JsonApiDocumentTopLevel
+ */
+class JsonApiDocumentTopLevelNormalizer extends NormalizerBase implements DenormalizerInterface, NormalizerInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $supportedInterfaceOrClass = JsonApiDocumentTopLevel::class;
+
+  /**
+   * The link manager to get the links.
+   *
+   * @var \Drupal\jsonapi\LinkManager\LinkManager
+   */
+  protected $linkManager;
+
+  /**
+   * The current JSON API request context.
+   *
+   * @var \Drupal\jsonapi\Context\CurrentContext
+   */
+  protected $currentContext;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructs a JsonApiDocumentTopLevelNormalizer object.
+   *
+   * @param \Drupal\jsonapi\LinkManager\LinkManager $link_manager
+   *   The link manager to get the links.
+   * @param \Drupal\jsonapi\Context\CurrentContext $current_context
+   *   The current context.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   */
+  public function __construct(LinkManager $link_manager, CurrentContext $current_context, EntityTypeManagerInterface $entity_type_manager, ResourceTypeRepository $resource_type_repository) {
+    $this->linkManager = $link_manager;
+    $this->currentContext = $current_context;
+    $this->entityTypeManager = $entity_type_manager;
+    $this->resourceTypeRepository = $resource_type_repository;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function denormalize($data, $class, $format = NULL, array $context = []) {
+    $context += [
+      'on_relationship' => $this->currentContext->isOnRelationship(),
+    ];
+    $normalized = [];
+    if (!empty($data['data']['attributes'])) {
+      $normalized = $data['data']['attributes'];
+    }
+    if (!empty($data['data']['relationships'])) {
+      // Turn all single object relationship data fields into an array of objects.
+      $relationships = array_map(function ($relationship) {
+        if (isset($relationship['data']['type']) && isset($relationship['data']['id'])) {
+          return ['data' => [$relationship['data']]];
+        }
+        else {
+          return $relationship;
+        }
+      }, $data['data']['relationships']);
+
+      // Get an array of ids for every relationship.
+      $relationships = array_map(function ($relationship) {
+        if (empty($relationship['data'])) {
+          return [];
+        }
+        if (empty($relationship['data'][0]['id'])) {
+          throw new BadRequestHttpException("No ID specified for related resource");
+        }
+        $id_list = array_column($relationship['data'], 'id');
+        if (empty($relationship['data'][0]['type'])) {
+          throw new BadRequestHttpException("No type specified for related resource");
+        }
+        if (!$resource_type = $this->resourceTypeRepository->getByTypeName($relationship['data'][0]['type'])) {
+          throw new BadRequestHttpException("Invalid type specified for related resource: '" . $relationship['data'][0]['type'] . "'");
+        }
+
+        $entity_type_id = $resource_type->getEntityTypeId();
+        try {
+          $entity_storage = $this->entityTypeManager->getStorage($entity_type_id);
+        }
+        catch (PluginNotFoundException $e) {
+          throw new BadRequestHttpException("Invalid type specified for related resource: '" . $relationship['data'][0]['type'] . "'");
+        }
+        // In order to maintain the order ($delta) of the relationships, we need
+        // to load the entities and create a mapping between id and uuid.
+        $related_entities = array_values($entity_storage->loadByProperties(['uuid' => $id_list]));
+        $map = [];
+        foreach ($related_entities as $related_entity) {
+          $map[$related_entity->uuid()] = $related_entity->id();
+        }
+
+        // $id_list has the correct order of uuids. We stitch this together with
+        // $map which contains loaded entities, and then bring in the correct
+        // meta values from the relationship, whose deltas match with $id_list.
+        $canonical_ids = [];
+        foreach ($id_list as $delta => $uuid) {
+          if (empty($map[$uuid])) {
+            continue;
+          }
+          $reference_item = [
+            'target_id' => $map[$uuid],
+          ];
+          if (isset($relationship['data'][$delta]['meta'])) {
+            $reference_item += $relationship['data'][$delta]['meta'];
+          }
+          $canonical_ids[] = $reference_item;
+        }
+
+        return array_filter($canonical_ids);
+      }, $relationships);
+
+      // Add the relationship ids.
+      $normalized = array_merge($normalized, $relationships);
+    }
+    // Override deserialization target class with the one in the ResourceType.
+    $class = $context['resource_type']->getDeserializationTargetClass();
+
+    return $this
+      ->serializer
+      ->denormalize($normalized, $class, $format, $context);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function normalize($object, $format = NULL, array $context = []) {
+    if (empty($context['resource_type'])) {
+      $context['resource_type'] = $this->currentContext->getResourceType();
+    }
+    $value_extractor = $this->buildNormalizerValue($object->getData(), $format, $context);
+    if (!empty($context['cacheable_metadata'])) {
+      $context['cacheable_metadata']->addCacheableDependency($value_extractor);
+    }
+    $normalized = $value_extractor->rasterizeValue();
+    $included = array_filter($value_extractor->rasterizeIncludes());
+    if (!empty($included)) {
+      $normalized['included'] = [];
+      foreach ($included as $included_item) {
+        if ($included_item['data'] === FALSE) {
+          unset($included_item['data']);
+          $normalized = NestedArray::mergeDeep($normalized, $included_item);
+        }
+        else {
+          $normalized['included'][] = $included_item['data'];
+        }
+      }
+    }
+
+    return $normalized;
+  }
+
+  /**
+   * Build the normalizer value.
+   *
+   * @return \Drupal\jsonapi\Normalizer\Value\JsonApiDocumentTopLevelNormalizerValue
+   *   The normalizer value.
+   */
+  public function buildNormalizerValue($data, $format = NULL, array $context = []) {
+    $context += $this->expandContext($context['request'], $context['resource_type']);
+    if ($data instanceof EntityReferenceFieldItemListInterface) {
+      $output = $this->serializer->normalize($data, $format, $context);
+      // The only normalizer value that computes nested includes automatically is the JsonApiDocumentTopLevelNormalizerValue.
+      $output->setIncludes($output->getAllIncludes());
+      return $output;
+    }
+    else {
+      $is_collection = $data instanceof EntityCollection;
+      $include_count = $context['resource_type']->includeCount();
+      // To improve the logical workflow deal with an array at all times.
+      $entities = $is_collection ? $data->toArray() : [$data];
+      $context['has_next_page'] = $is_collection ? $data->hasNextPage() : FALSE;
+
+      if ($include_count) {
+        $context['total_count'] = $is_collection ? $data->getTotalCount() : 1;
+      }
+      $serializer = $this->serializer;
+      $normalizer_values = array_map(function ($entity) use ($format, $context, $serializer) {
+        return $serializer->normalize($entity, $format, $context);
+      }, $entities);
+    }
+
+    $link_context = [
+      'link_manager' => $this->linkManager,
+      'has_next_page' => $context['has_next_page'],
+    ];
+
+    if ($include_count) {
+      $link_context['total_count'] = $context['total_count'];
+    }
+
+    return new JsonApiDocumentTopLevelNormalizerValue($normalizer_values, $context, $is_collection, $link_context);
+  }
+
+  /**
+   * Expand the context information based on the current request context.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request to get the URL params from to expand the context.
+   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
+   *   The resource type to translate to internal fields.
+   *
+   * @return array
+   *   The expanded context.
+   */
+  protected function expandContext(Request $request, ResourceType $resource_type) {
+    // Translate ALL the includes from the public field names to the internal.
+    $includes = array_filter(explode(',', $request->query->get('include')));
+    $public_includes = array_map(function ($include_str) use ($resource_type) {
+      $field_names = explode('.', $include_str);
+      return implode('.', array_map(
+        function ($field_name) use ($resource_type) {
+          return $resource_type->getInternalName($field_name);
+        },
+        $field_names
+      ));
+    }, $includes);
+    // Build the expanded context.
+    $context = [
+      'account' => NULL,
+      'sparse_fieldset' => NULL,
+      'resource_type' => NULL,
+      'include' => $public_includes,
+    ];
+    if ($request->query->get('fields')) {
+      $context['sparse_fieldset'] = array_map(function ($item) {
+        return explode(',', $item);
+      }, $request->query->get('fields'));
+    }
+
+    return $context;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Normalizer/NormalizerBase.php b/drupal/modules/jsonapi/src/Normalizer/NormalizerBase.php
new file mode 100644
index 0000000..a9f9b7b
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Normalizer/NormalizerBase.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Drupal\jsonapi\Normalizer;
+
+use Drupal\serialization\Normalizer\NormalizerBase as SerializationNormalizerBase;
+
+/**
+ * Base normalizer used in all JSON API normalizers.
+ */
+abstract class NormalizerBase extends SerializationNormalizerBase {
+
+  /**
+   * The formats that the Normalizer can handle.
+   *
+   * @var array
+   */
+  protected $formats = ['api_json'];
+
+  /**
+   * The JSON API resource type repository.
+   *
+   * @var \Drupal\jsonapi\ResourceType\ResourceTypeRepository
+   */
+  protected $resourceTypeRepository;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function supportsNormalization($data, $format = NULL) {
+    return in_array($format, $this->formats, TRUE) && parent::supportsNormalization($data, $format);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function supportsDenormalization($data, $type, $format = NULL) {
+    if (in_array($format, $this->formats, TRUE) && (class_exists($this->supportedInterfaceOrClass) || interface_exists($this->supportedInterfaceOrClass))) {
+      $target = new \ReflectionClass($type);
+      $supported = new \ReflectionClass($this->supportedInterfaceOrClass);
+      if ($supported->isInterface()) {
+        return $target->implementsInterface($this->supportedInterfaceOrClass);
+      }
+      else {
+        return ($target->getName() == $this->supportedInterfaceOrClass || $target->isSubclassOf($this->supportedInterfaceOrClass));
+      }
+    }
+
+    return FALSE;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Normalizer/Relationship.php b/drupal/modules/jsonapi/src/Normalizer/Relationship.php
new file mode 100644
index 0000000..7ef33a4
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Normalizer/Relationship.php
@@ -0,0 +1,145 @@
+<?php
+
+namespace Drupal\jsonapi\Normalizer;
+
+use Drupal\Core\Access\AccessibleInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\jsonapi\ResourceType\ResourceTypeRepository;
+use Drupal\jsonapi\Resource\EntityCollection;
+
+/**
+ * Use this class to create a relationship in your normalizer without having an
+ * entity reference field: allows for "virtual" relationships that are not
+ * backed by a stored entity reference.
+ *
+ * @internal
+ */
+class Relationship implements AccessibleInterface {
+
+  /**
+   * Cardinality.
+   *
+   * @var int
+   */
+  protected $cardinality;
+
+  /**
+   * The entity that holds the relationship.
+   *
+   * @var \Drupal\Core\Entity\EntityInterface
+   */
+  protected $hostEntity;
+
+  /**
+   * The field name.
+   *
+   * @var string
+   */
+  protected $propertyName;
+
+  /**
+   * The JSON API resource type repository.
+   *
+   * @var \Drupal\jsonapi\ResourceType\ResourceTypeRepository
+   */
+  protected $resourceTypeRepository;
+
+  /**
+   * The relationship items.
+   *
+   * @var \Drupal\jsonapi\Normalizer\RelationshipItem[]
+   */
+  protected $items;
+
+  /**
+   * Relationship constructor.
+   *
+   * @param \Drupal\jsonapi\ResourceType\ResourceTypeRepository $resource_type_repository
+   *   The JSON API resource type repository.
+   * @param string $field_name
+   *   The name of the relationship.
+   * @param int $cardinality
+   *   The relationship cardinality.
+   * @param \Drupal\jsonapi\Resource\EntityCollection $entities
+   *   A collection of entities.
+   * @param \Drupal\Core\Entity\EntityInterface $host_entity
+   *   The host entity.
+   * @param string $target_key
+   *   The property name of the relationship id.
+   * @param array $entity_list_metadata
+   *   An array of additional properties stored by the field and that will be
+   *   added to the meta in the relationship.
+   */
+  public function __construct(ResourceTypeRepository $resource_type_repository, $field_name, $cardinality = FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, EntityCollection $entities, EntityInterface $host_entity, $target_key = 'target_id', array $entity_list_metadata = []) {
+    $this->resourceTypeRepository = $resource_type_repository;
+    $this->propertyName = $field_name;
+    $this->cardinality = $cardinality;
+    $this->hostEntity = $host_entity;
+    $this->items = [];
+    foreach ($entities as $key => $entity) {
+      $this->items[] = new RelationshipItem(
+        $resource_type_repository,
+        $entity,
+        $this,
+        $target_key,
+        $entity_list_metadata[$key]
+      );
+    }
+  }
+
+  /**
+   * Gets the cardinality.
+   *
+   * @return mixed
+   */
+  public function getCardinality() {
+    return $this->cardinality;
+  }
+
+  /**
+   * Gets the host entity.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface
+   */
+  public function getHostEntity() {
+    return $this->hostEntity;
+  }
+
+  /**
+   * Sets the host entity.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $hostEntity
+   */
+  public function setHostEntity(EntityInterface $hostEntity) {
+    $this->hostEntity = $hostEntity;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) {
+    // Hard coded to TRUE. Revisit this if we need more control over this.
+    return TRUE;
+  }
+
+  /**
+   * Gets the field name.
+   *
+   * @return string
+   */
+  public function getPropertyName() {
+    return $this->propertyName;
+  }
+
+  /**
+   * Gets the items.
+   *
+   * @return \Drupal\jsonapi\Normalizer\RelationshipItem[]
+   */
+  public function getItems() {
+    return $this->items;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Normalizer/RelationshipItem.php b/drupal/modules/jsonapi/src/Normalizer/RelationshipItem.php
new file mode 100644
index 0000000..9f8ecdb
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Normalizer/RelationshipItem.php
@@ -0,0 +1,114 @@
+<?php
+
+namespace Drupal\jsonapi\Normalizer;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\jsonapi\ResourceType\ResourceTypeRepository;
+
+/**
+ * @internal
+ */
+class RelationshipItem {
+
+  /**
+   * The target key name.
+   *
+   * @param string
+   */
+  protected $targetKey = 'target_id';
+
+  /**
+   * The target entity.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface
+   */
+  protected $targetEntity;
+
+  /**
+   * The target JSON API resource type.
+   *
+   * @param \Drupal\jsonapi\ResourceType\ResourceType
+   */
+  protected $targetResourceType;
+
+  /**
+   * The parent relationship.
+   *
+   * @var \Drupal\jsonapi\Normalizer\Relationship
+   */
+  protected $parent;
+
+  /**
+   * The list of metadata associated with this relationship item value.
+   *
+   * @var array
+   */
+  protected $metadata;
+
+  /**
+   * Relationship item constructor.
+   *
+   * @param \Drupal\jsonapi\ResourceType\ResourceTypeRepository $resource_type_repository
+   *   The JSON API resource type repository.
+   * @param \Drupal\Core\Entity\EntityInterface $target_entity
+   *   The entity this relationship points to.
+   * @param \Drupal\jsonapi\Normalizer\Relationship $parent
+   *   The parent of this item.
+   * @param string $target_key
+   *   The key name of the target relationship.
+   * @param array $metadata
+   *   The list of metadata associated with this relationship item value.
+   */
+  public function __construct(ResourceTypeRepository $resource_type_repository, EntityInterface $target_entity, Relationship $parent, $target_key = 'target_id', array $metadata = []) {
+    $this->targetResourceType = $resource_type_repository->get(
+      $target_entity->getEntityTypeId(),
+      $target_entity->bundle()
+    );
+    $this->targetKey = $target_key;
+    $this->targetEntity = $target_entity;
+    $this->parent = $parent;
+    $this->metadata = $metadata;
+  }
+
+  /**
+   * Gets the target entity.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface
+   */
+  public function getTargetEntity() {
+    return $this->targetEntity;
+  }
+
+  /**
+   * Gets the targetResourceConfig.
+   *
+   * @return mixed
+   */
+  public function getTargetResourceType() {
+    return $this->targetResourceType;
+  }
+
+  /**
+   * Gets the relationship value.
+   *
+   * Defaults to the entity ID.
+   *
+   * @return string
+   */
+  public function getValue() {
+    return [
+      'target_uuid' => $this->getTargetEntity()->uuid(),
+      'meta' => $this->metadata,
+    ];
+  }
+
+  /**
+   * Gets the relationship object that contains this relationship item.
+   *
+   * @return \Drupal\jsonapi\Normalizer\Relationship
+   */
+  public function getParent() {
+    return $this->parent;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Normalizer/RelationshipItemNormalizer.php b/drupal/modules/jsonapi/src/Normalizer/RelationshipItemNormalizer.php
new file mode 100644
index 0000000..864c52a
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Normalizer/RelationshipItemNormalizer.php
@@ -0,0 +1,139 @@
+<?php
+
+namespace Drupal\jsonapi\Normalizer;
+
+use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
+use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\jsonapi\Normalizer\Value\RelationshipItemNormalizerValue;
+use Drupal\jsonapi\ResourceType\ResourceTypeRepository;
+use Drupal\jsonapi\Controller\EntityResource;
+use Drupal\serialization\EntityResolver\UuidReferenceInterface;
+
+/**
+ * Converts the Drupal entity reference item object to a JSON API structure.
+ *
+ * @todo Remove the dependency on \Drupal\jsonapi\Normalizer\JsonApiDocumentTopLevelNormalizer
+ */
+class RelationshipItemNormalizer extends FieldItemNormalizer implements UuidReferenceInterface, RefinableCacheableDependencyInterface {
+
+  use RefinableCacheableDependencyTrait;
+
+  /**
+   * The interface or class that this Normalizer supports.
+   *
+   * @var string
+   */
+  protected $supportedInterfaceOrClass = RelationshipItem::class;
+
+  /**
+   * The JSON API resource type repository.
+   *
+   * @var \Drupal\jsonapi\ResourceType\ResourceTypeRepository
+   */
+  protected $resourceTypeRepository;
+
+  /**
+   * The JSON API document top level normalizer.
+   *
+   * @var \Drupal\jsonapi\Normalizer\JsonApiDocumentTopLevelNormalizer
+   */
+  protected $jsonapiDocumentToplevelNormalizer;
+
+  /**
+   * Instantiates a RelationshipItemNormalizer object.
+   *
+   * @param \Drupal\jsonapi\ResourceType\ResourceTypeRepository $resource_type_repository
+   *   The JSON API resource type repository.
+   * @param \Drupal\jsonapi\Normalizer\JsonApiDocumentTopLevelNormalizer $jsonapi_document_toplevel_normalizer
+   *   The document root normalizer for the include.
+   */
+  public function __construct(ResourceTypeRepository $resource_type_repository, JsonApiDocumentTopLevelNormalizer $jsonapi_document_toplevel_normalizer) {
+    $this->resourceTypeRepository = $resource_type_repository;
+    $this->jsonapiDocumentToplevelNormalizer = $jsonapi_document_toplevel_normalizer;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function normalize($relationship_item, $format = NULL, array $context = []) {
+    /* @var $relationship_item \Drupal\jsonapi\Normalizer\RelationshipItem */
+    // TODO: We are always loading the referenced entity. Even if it is not
+    // going to be included. That may be a performance issue. We do it because
+    // we need to know the entity type and bundle to load the JSON API resource
+    // type for the relationship item. We need a better way of finding about
+    // this.
+    $target_entity = $relationship_item->getTargetEntity();
+    $values = $relationship_item->getValue();
+    if (isset($context['langcode'])) {
+      $values['lang'] = $context['langcode'];
+    }
+    $normalizer_value = new RelationshipItemNormalizerValue(
+      $values,
+      $relationship_item->getTargetResourceType()
+    );
+
+    $host_field_name = $relationship_item->getParent()->getPropertyName();
+    if (!empty($context['include']) && in_array($host_field_name, $context['include'])) {
+      $context = $this->buildSubContext($context, $target_entity, $host_field_name);
+      $entity_and_access = EntityResource::getEntityAndAccess($target_entity);
+      $included_normalizer_value = $this
+        ->jsonapiDocumentToplevelNormalizer
+        ->buildNormalizerValue($entity_and_access['entity'], $format, $context);
+      $normalizer_value->setInclude($included_normalizer_value);
+      $normalizer_value->addCacheableDependency($entity_and_access['access']);
+      $normalizer_value->addCacheableDependency($included_normalizer_value);
+      // Add the cacheable dependency of the included item directly to the
+      // response cacheable metadata. This is similar to the flatten include
+      // data structure, instead of a content graph.
+      if (!empty($context['cacheable_metadata'])) {
+        $context['cacheable_metadata']->addCacheableDependency($normalizer_value);
+      }
+    }
+    return $normalizer_value;
+  }
+
+  /**
+   * Builds the sub-context for the relationship include.
+   *
+   * @param array $context
+   *   The serialization context.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The related entity.
+   * @param string $host_field_name
+   *   The name of the field reference.
+   *
+   * @return array
+   *   The modified new context.
+   */
+  protected function buildSubContext(array $context, EntityInterface $entity, $host_field_name) {
+    // Swap out the context for the context of the referenced resource.
+    $context['resource_type'] = $this->resourceTypeRepository
+      ->get($entity->getEntityTypeId(), $entity->bundle());
+    // Since we're going one level down the only includes we need are the ones
+    // that apply to this level as well.
+    $include_candidates = array_filter($context['include'], function ($include) use ($host_field_name) {
+      return strpos($include, $host_field_name . '.') === 0;
+    });
+    $context['include'] = array_map(function ($include) use ($host_field_name) {
+      return str_replace($host_field_name . '.', '', $include);
+    }, $include_candidates);
+    return $context;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getUuid($data) {
+    if (isset($data['uuid'])) {
+      return NULL;
+    }
+    $uuid = $data['uuid'];
+    // The value may be a nested array like $uuid[0]['value'].
+    if (is_array($uuid) && isset($uuid[0]['value'])) {
+      $uuid = $uuid[0]['value'];
+    }
+    return $uuid;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Normalizer/RelationshipNormalizer.php b/drupal/modules/jsonapi/src/Normalizer/RelationshipNormalizer.php
new file mode 100644
index 0000000..5ea3dd0
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Normalizer/RelationshipNormalizer.php
@@ -0,0 +1,118 @@
+<?php
+
+namespace Drupal\jsonapi\Normalizer;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\jsonapi\Normalizer\Value\RelationshipNormalizerValue;
+use Drupal\jsonapi\ResourceType\ResourceTypeRepository;
+use Drupal\jsonapi\LinkManager\LinkManager;
+use Symfony\Component\Serializer\Exception\UnexpectedValueException;
+
+/**
+ * Normalizer class for relationship elements. A relationship can be anything
+ * that points to an entity in a JSON API resource.
+ */
+class RelationshipNormalizer extends NormalizerBase {
+
+  /**
+   * The interface or class that this Normalizer supports.
+   *
+   * @var string
+   */
+  protected $supportedInterfaceOrClass = Relationship::class;
+
+  /**
+   * The formats that the Normalizer can handle.
+   *
+   * @var array
+   */
+  protected $formats = ['api_json'];
+
+  /**
+   * The link manager.
+   *
+   * @var \Drupal\jsonapi\LinkManager\LinkManager
+   */
+  protected $linkManager;
+
+  /**
+   * RelationshipNormalizer constructor.
+   *
+   * @param \Drupal\jsonapi\ResourceType\ResourceTypeRepository $resource_type_repository
+   *   The JSON API resource type repository.
+   * @param \Drupal\jsonapi\LinkManager\LinkManager $link_manager
+   *   The link manager.
+   */
+  public function __construct(ResourceTypeRepository $resource_type_repository, LinkManager $link_manager) {
+    $this->resourceTypeRepository = $resource_type_repository;
+    $this->linkManager = $link_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function denormalize($data, $class, $format = NULL, array $context = []) {
+    throw new UnexpectedValueException('Denormalization not implemented for JSON API');
+  }
+
+  /**
+   * Helper function to normalize field items.
+   *
+   * @param \Drupal\jsonapi\Normalizer\Relationship $relationship
+   *   The field object.
+   * @param string $format
+   *   The format.
+   * @param array $context
+   *   The context array.
+   *
+   * @return \Drupal\jsonapi\Normalizer\Value\RelationshipNormalizerValue
+   *   The array of normalized field items.
+   */
+  public function normalize($relationship, $format = NULL, array $context = []) {
+    /* @var \Drupal\jsonapi\Normalizer\Relationship $relationship */
+    $normalizer_items = [];
+    foreach ($relationship->getItems() as $relationship_item) {
+      $normalizer_items[] = $this->serializer->normalize($relationship_item, $format, $context);
+    }
+    $cardinality = $relationship->getCardinality();
+    $link_context = [
+      'host_entity_id' => $relationship->getHostEntity()->uuid(),
+      'field_name' => $relationship->getPropertyName(),
+      'link_manager' => $this->linkManager,
+      'resource_type' => $context['resource_type'],
+    ];
+    return new RelationshipNormalizerValue($normalizer_items, $cardinality, $link_context);
+  }
+
+  /**
+   * Builds the sub-context for the relationship include.
+   *
+   * @param array $context
+   *   The serialization context.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The related entity.
+   * @param string $host_field_name
+   *   The name of the field reference.
+   *
+   * @return array
+   *   The modified new context.
+   *
+   * @see EntityReferenceItemNormalizer::buildSubContext()
+   * @todo This is duplicated code from the reference item. Reuse code instead.
+   */
+  protected function buildSubContext(array $context, EntityInterface $entity, $host_field_name) {
+    // Swap out the context for the context of the referenced resource.
+    $context['resource_type'] = $this->resourceTypeRepository
+      ->get($entity->getEntityTypeId(), $entity->bundle());
+    // Since we're going one level down the only includes we need are the ones
+    // that apply to this level as well.
+    $include_candidates = array_filter($context['include'], function ($include) use ($host_field_name) {
+      return strpos($include, $host_field_name . '.') === 0;
+    });
+    $context['include'] = array_map(function ($include) use ($host_field_name) {
+      return str_replace($host_field_name . '.', '', $include);
+    }, $include_candidates);
+    return $context;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Normalizer/ScalarNormalizer.php b/drupal/modules/jsonapi/src/Normalizer/ScalarNormalizer.php
new file mode 100644
index 0000000..e34750a
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Normalizer/ScalarNormalizer.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Drupal\jsonapi\Normalizer;
+
+use Drupal\jsonapi\Normalizer\Value\FieldItemNormalizerValue;
+use Drupal\jsonapi\Normalizer\Value\FieldNormalizerValue;
+use Symfony\Component\Serializer\Exception\UnexpectedValueException;
+
+/**
+ * The normalizer used for scalar inputs.
+ */
+class ScalarNormalizer extends NormalizerBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $formats = ['api_json'];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function supportsNormalization($data, $format = NULL) {
+    return (!$data || is_scalar($data)) && in_array($format, $this->formats);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function normalize($object, $format = NULL, array $context = []) {
+    $value = new FieldItemNormalizerValue(['value' => $object]);
+    return new FieldNormalizerValue([$value], 1);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function denormalize($data, $class, $format = NULL, array $context = []) {
+    throw new UnexpectedValueException('Denormalization not implemented for JSON API');
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Normalizer/UnprocessableHttpEntityExceptionNormalizer.php b/drupal/modules/jsonapi/src/Normalizer/UnprocessableHttpEntityExceptionNormalizer.php
new file mode 100644
index 0000000..2326d81
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Normalizer/UnprocessableHttpEntityExceptionNormalizer.php
@@ -0,0 +1,84 @@
+<?php
+
+namespace Drupal\jsonapi\Normalizer;
+
+use Drupal\Core\Session\AccountProxyInterface;
+use Drupal\jsonapi\Exception\UnprocessableHttpEntityException;
+use Symfony\Component\HttpKernel\Exception\HttpException;
+
+/**
+ * Normalizes an UnprocessableHttpEntityException object for JSON output which
+ * complies with the JSON API specification. A source pointer is added to help
+ * client applications report validation errors, for example on an Entity edit
+ * form.
+ *
+ * @see http://jsonapi.org/format/#error-objects
+ */
+class UnprocessableHttpEntityExceptionNormalizer extends HttpExceptionNormalizer {
+
+  /**
+   * The interface or class that this Normalizer supports.
+   *
+   * @var string
+   */
+  protected $supportedInterfaceOrClass = UnprocessableHttpEntityException::class;
+
+  /**
+   * UnprocessableHttpEntityException constructor.
+   *
+   * @param \Drupal\Core\Session\AccountProxyInterface $current_user
+   *   The current user.
+   */
+  public function __construct(AccountProxyInterface $current_user) {
+    parent::__construct($current_user);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function buildErrorObjects(HttpException $exception) {
+    /** @var $exception \Drupal\jsonapi\Exception\UnprocessableHttpEntityException */
+    $errors = parent::buildErrorObjects($exception);
+    $error = $errors[0];
+    unset($error['links']);
+
+    $errors = [];
+    $violations = $exception->getViolations();
+    $entity_violations = $violations->getEntityViolations();
+    foreach ($entity_violations as $violation) {
+      /** @var \Symfony\Component\Validator\ConstraintViolation $violation */
+      $error['detail'] = 'Entity is not valid: '
+        . $violation->getMessage();
+      $error['source']['pointer'] = '/data';
+      $errors[] = $error;
+    }
+
+    $entity = $violations->getEntity();
+    foreach ($violations->getFieldNames() as $field_name) {
+      $field_violations = $violations->getByField($field_name);
+      $cardinality = $entity->get($field_name)
+        ->getFieldDefinition()
+        ->getFieldStorageDefinition()
+        ->getCardinality();
+
+      foreach ($field_violations as $violation) {
+        /** @var \Symfony\Component\Validator\ConstraintViolation $violation */
+        $error['detail'] = $violation->getPropertyPath() . ': '
+          . $violation->getMessage();
+
+        $pointer = '/data/attributes/'
+          . str_replace('.', '/', $violation->getPropertyPath());
+        if ($cardinality == 1) {
+          // Remove erroneous '/0/' index for single-value fields.
+          $pointer = str_replace("/data/attributes/$field_name/0/", "/data/attributes/$field_name/", $pointer);
+        }
+        $error['source']['pointer'] = $pointer;
+
+        $errors[] = $error;
+      }
+    }
+
+    return $errors;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Normalizer/Value/EntityNormalizerValue.php b/drupal/modules/jsonapi/src/Normalizer/Value/EntityNormalizerValue.php
new file mode 100644
index 0000000..f2850fd
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Normalizer/Value/EntityNormalizerValue.php
@@ -0,0 +1,147 @@
+<?php
+
+namespace Drupal\jsonapi\Normalizer\Value;
+
+use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
+use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * @internal
+ */
+class EntityNormalizerValue implements ValueExtractorInterface, RefinableCacheableDependencyInterface {
+
+  use RefinableCacheableDependencyTrait;
+
+  /**
+   * The values.
+   *
+   * @param array
+   */
+  protected $values;
+
+  /**
+   * The includes.
+   *
+   * @param array
+   */
+  protected $includes;
+
+  /**
+   * The resource path.
+   *
+   * @param array
+   */
+  protected $context;
+
+  /**
+   * The resource entity.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface
+   */
+  protected $entity;
+
+  /**
+   * The link manager.
+   *
+   * @param \Drupal\jsonapi\LinkManager\LinkManager
+   */
+  protected $linkManager;
+
+  /**
+   * Instantiate a EntityNormalizerValue object.
+   *
+   * @param FieldNormalizerValueInterface[] $values
+   *   The normalized result.
+   * @param array $context
+   *   The context for the normalizer.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity.
+   * @param array $link_context
+   *   All the objects and variables needed to generate the links for this
+   *   relationship.
+   */
+  public function __construct(array $values, array $context, EntityInterface $entity, array $link_context) {
+    $this->values = array_filter($values, function ($value) {
+      return !($value instanceof NullFieldNormalizerValue);
+    });
+    $this->context = $context;
+    $this->entity = $entity;
+    $this->linkManager = $link_context['link_manager'];
+    // Get an array of arrays of includes.
+    $this->includes = array_map(function ($value) {
+      return $value->getIncludes();
+    }, $values);
+    // Flatten the includes.
+    $this->includes = array_reduce($this->includes, function ($carry, $includes) {
+      return array_merge($carry, $includes);
+    }, []);
+    // Filter the empty values.
+    $this->includes = array_filter($this->includes);
+    array_walk($this->includes, function ($include) {
+      $this->addCacheableDependency($include);
+    });
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function rasterizeValue() {
+    // Create the array of normalized fields, starting with the URI.
+    $rasterized = [
+      'type' => $this->context['resource_type']->getTypeName(),
+      'id' => $this->entity->uuid(),
+      'attributes' => [],
+      'relationships' => [],
+    ];
+    $rasterized['links'] = [
+      'self' => $this->linkManager->getEntityLink(
+        $rasterized['id'],
+        $this->context['resource_type'],
+        [],
+        'individual'
+      ),
+    ];
+
+    foreach ($this->getValues() as $field_name => $normalizer_value) {
+      $rasterized[$normalizer_value->getPropertyType()][$field_name] = $normalizer_value->rasterizeValue();
+    }
+    return array_filter($rasterized);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function rasterizeIncludes() {
+    // First gather all the includes in the chain.
+    return array_map(function ($include) {
+      return $include->rasterizeValue();
+    }, $this->getIncludes());
+  }
+
+  /**
+   * Gets the values.
+   *
+   * @return mixed
+   *   The values.
+   */
+  public function getValues() {
+    return $this->values;
+  }
+
+  /**
+   * Gets a flattened list of includes in all the chain.
+   *
+   * @return \Drupal\jsonapi\Normalizer\Value\EntityNormalizerValue[]
+   *   The array of included relationships.
+   */
+  public function getIncludes() {
+    $nested_includes = array_map(function ($include) {
+      return $include->getIncludes();
+    }, $this->includes);
+    return array_reduce(array_filter($nested_includes), function ($carry, $item) {
+      return array_merge($carry, $item);
+    }, $this->includes);
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Normalizer/Value/FieldItemNormalizerValue.php b/drupal/modules/jsonapi/src/Normalizer/Value/FieldItemNormalizerValue.php
new file mode 100644
index 0000000..0291982
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Normalizer/Value/FieldItemNormalizerValue.php
@@ -0,0 +1,107 @@
+<?php
+
+namespace Drupal\jsonapi\Normalizer\Value;
+
+/**
+ * @internal
+ */
+class FieldItemNormalizerValue implements ValueExtractorInterface {
+
+  /**
+   * Raw values.
+   *
+   * @param array
+   */
+  protected $raw;
+
+  /**
+   * Included entity objects.
+   *
+   * @param \Drupal\jsonapi\Normalizer\Value\EntityNormalizerValue
+   */
+  protected $include;
+
+  /**
+   * Instantiate a FieldItemNormalizerValue object.
+   *
+   * @param array $values
+   *   The normalized result.
+   */
+  public function __construct(array $values) {
+    $this->raw = $values;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function rasterizeValue() {
+    // If there is only one property, then output it directly.
+    $value = count($this->raw) == 1 ? reset($this->raw) : $this->raw;
+
+    return $this->rasterizeValueRecursive($value);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function rasterizeIncludes() {
+    return $this->include->rasterizeValue();
+  }
+
+  /**
+   * Add an include.
+   *
+   * @param ValueExtractorInterface $include
+   *   The included entity.
+   */
+  public function setInclude(ValueExtractorInterface $include) {
+    $this->include = $include;
+  }
+
+  /**
+   * Gets the include.
+   *
+   * @return \Drupal\jsonapi\Normalizer\Value\EntityNormalizerValue
+   *   The include.
+   */
+  public function getInclude() {
+    return $this->include;
+  }
+
+  /**
+   * Rasterizes a value recursively.
+   *
+   * This is mainly for configuration entities where a field can be a tree of
+   * values to rasterize.
+   *
+   * @param mixed $value
+   *   Either a scalar, an array or a rasterizable object.
+   *
+   * @return mixed
+   *   The rasterized value.
+   */
+  protected function rasterizeValueRecursive($value) {
+    if (!$value || is_scalar($value)) {
+      return $value;
+    }
+    if (is_array($value)) {
+      $output = [];
+      foreach ($value as $key => $item) {
+        $output[$key] = $this->rasterizeValueRecursive($item);
+      }
+
+      return $output;
+    }
+    if ($value instanceof ValueExtractorInterface) {
+      return $value->rasterizeValue();
+    }
+    // If the object can be turned into a string it's better than nothing.
+    if (method_exists($value, '__toString')) {
+      return $value->__toString();
+    }
+
+    // We give up, since we do not know how to rasterize this.
+    return NULL;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Normalizer/Value/FieldNormalizerValue.php b/drupal/modules/jsonapi/src/Normalizer/Value/FieldNormalizerValue.php
new file mode 100644
index 0000000..a3697d8
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Normalizer/Value/FieldNormalizerValue.php
@@ -0,0 +1,128 @@
+<?php
+
+namespace Drupal\jsonapi\Normalizer\Value;
+
+use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
+
+/**
+ * @internal
+ */
+class FieldNormalizerValue implements FieldNormalizerValueInterface {
+
+  use RefinableCacheableDependencyTrait;
+
+  /**
+   * The values.
+   *
+   * @param array
+   */
+  protected $values;
+
+  /**
+   * The includes.
+   *
+   * @param array
+   */
+  protected $includes;
+
+  /**
+   * The field cardinality.
+   *
+   * @param integer
+   */
+  protected $cardinality;
+
+  /**
+   * The property type. Either: 'attributes' or `relationships'.
+   *
+   * @var string
+   */
+  protected $propertyType;
+
+  /**
+   * Instantiate a FieldNormalizerValue object.
+   *
+   * @param \Drupal\jsonapi\Normalizer\Value\FieldItemNormalizerValue[] $values
+   *   The normalized result.
+   * @param int $cardinality
+   *   The cardinality of the field list.
+   */
+  public function __construct(array $values, $cardinality) {
+    $this->values = $values;
+    $this->includes = array_map(function ($value) {
+      return $value->getInclude();
+    }, $values);
+    $this->includes = array_filter($this->includes);
+    $this->cardinality = $cardinality;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function rasterizeValue() {
+    if (empty($this->values)) {
+      return NULL;
+    }
+    return $this->cardinality == 1 ?
+      $this->values[0]->rasterizeValue() :
+      array_map(function ($value) {
+        return $value->rasterizeValue();
+      }, $this->values);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function rasterizeIncludes() {
+    return array_map(function ($include) {
+      return $include->rasterizeValue();
+    }, $this->includes);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getIncludes() {
+    return $this->includes;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPropertyType() {
+    return $this->propertyType;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setPropertyType($property_type) {
+    $this->propertyType = $property_type;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setIncludes($includes) {
+    $this->includes = $includes;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getAllIncludes() {
+    $nested_includes = array_map(function ($include) {
+      return $include->getIncludes();
+    }, $this->getIncludes());
+    $includes = array_reduce(array_filter($nested_includes), function ($carry, $item) {
+      return array_merge($carry, $item);
+    }, $this->getIncludes());
+    // Make sure we don't output duplicate includes.
+    return array_values(array_reduce($includes, function ($unique_includes, $include) {
+      $rasterized_include = $include->rasterizeValue();
+      $unique_includes[$rasterized_include['data']['type'] . ':' . $rasterized_include['data']['id']] = $include;
+      return $unique_includes;
+    }, []));
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Normalizer/Value/FieldNormalizerValueInterface.php b/drupal/modules/jsonapi/src/Normalizer/Value/FieldNormalizerValueInterface.php
new file mode 100644
index 0000000..7875dd7
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Normalizer/Value/FieldNormalizerValueInterface.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Drupal\jsonapi\Normalizer\Value;
+
+use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
+
+/**
+ * @internal
+ */
+interface FieldNormalizerValueInterface extends ValueExtractorInterface, RefinableCacheableDependencyInterface {
+
+  /**
+   * Gets the includes.
+   *
+   * @return mixed
+   *   The includes.
+   */
+  public function getIncludes();
+
+  /**
+   * Gets the propertyType.
+   *
+   * @return mixed
+   *   The propertyType.
+   */
+  public function getPropertyType();
+
+  /**
+   * Sets the propertyType.
+   *
+   * @param mixed $property_type
+   *   The propertyType to set.
+   */
+  public function setPropertyType($property_type);
+
+  /**
+   * Sets the includes.
+   *
+   * This is used to manually set the nested includes when using the
+   * relationship as a document root in a
+   * /{resource}/{id}/relationships/{fieldName}.
+   *
+   * @param array $includes
+   *   The includes.
+   */
+  public function setIncludes($includes);
+
+  /**
+   * Computes all the nested includes recursively.
+   *
+   * @return array
+   *   The includes and the nested includes.
+   */
+  public function getAllIncludes();
+
+}
diff --git a/drupal/modules/jsonapi/src/Normalizer/Value/HttpExceptionNormalizerValue.php b/drupal/modules/jsonapi/src/Normalizer/Value/HttpExceptionNormalizerValue.php
new file mode 100644
index 0000000..9815603
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Normalizer/Value/HttpExceptionNormalizerValue.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace Drupal\jsonapi\Normalizer\Value;
+
+/**
+ * @internal
+ */
+class HttpExceptionNormalizerValue extends FieldNormalizerValue {}
diff --git a/drupal/modules/jsonapi/src/Normalizer/Value/JsonApiDocumentTopLevelNormalizerValue.php b/drupal/modules/jsonapi/src/Normalizer/Value/JsonApiDocumentTopLevelNormalizerValue.php
new file mode 100644
index 0000000..78c7972
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Normalizer/Value/JsonApiDocumentTopLevelNormalizerValue.php
@@ -0,0 +1,179 @@
+<?php
+
+namespace Drupal\jsonapi\Normalizer\Value;
+
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
+use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
+use Drupal\jsonapi\JsonApiSpec;
+
+/**
+ * @internal
+ */
+class JsonApiDocumentTopLevelNormalizerValue implements ValueExtractorInterface, RefinableCacheableDependencyInterface {
+
+  use RefinableCacheableDependencyTrait;
+
+  /**
+   * The values.
+   *
+   * @param array
+   */
+  protected $values;
+
+  /**
+   * The includes.
+   *
+   * @param array
+   */
+  protected $includes;
+
+  /**
+   * The resource path.
+   *
+   * @param array
+   */
+  protected $context;
+
+  /**
+   * Is collection?
+   *
+   * @param bool
+   */
+  protected $isCollection;
+
+  /**
+   * The link manager.
+   *
+   * @var \Drupal\jsonapi\LinkManager\LinkManager
+   */
+  protected $linkManager;
+
+  /**
+   * The link context.
+   *
+   * @var array
+   */
+  protected $linkContext;
+
+  /**
+   * Instantiates a JsonApiDocumentTopLevelNormalizerValue object.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface[] $values
+   *   The data to normalize. It can be either a straight up entity or a
+   *   collection of entities.
+   * @param array $context
+   *   The context.
+   * @param bool $is_collection
+   *   TRUE if this is a serialization for a list.
+   * @param array $link_context
+   *   All the objects and variables needed to generate the links for this
+   *   relationship.
+   */
+  public function __construct(array $values, array $context, $is_collection = FALSE, array $link_context) {
+    $this->values = $values;
+    array_walk($values, [$this, 'addCacheableDependency']);
+    // Make sure that different sparse fieldsets are cached differently.
+    $this->addCacheContexts(array_map(function ($query_parameter_name) {
+      return sprintf('url.query_args:%s', $query_parameter_name);
+    }, JsonApiSpec::getReservedQueryParameters()));
+
+    $this->context = $context;
+    $this->isCollection = $is_collection;
+    $this->linkManager = $link_context['link_manager'];
+    // Remove the manager and store the link context.
+    unset($link_context['link_manager']);
+    $this->linkContext = $link_context;
+    // Get an array of arrays of includes.
+    $this->includes = array_map(function ($value) {
+      return $value->getIncludes();
+    }, $values);
+    // Flatten the includes.
+    $this->includes = array_reduce($this->includes, function ($carry, $includes) {
+      array_walk($includes, [$this, 'addCacheableDependency']);
+      return array_merge($carry, $includes);
+    }, []);
+    // Filter the empty values.
+    $this->includes = array_filter($this->includes);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function rasterizeValue() {
+    // Create the array of normalized fields, starting with the URI.
+    $rasterized = ['data' => []];
+
+    foreach ($this->values as $normalizer_value) {
+      if ($normalizer_value instanceof HttpExceptionNormalizerValue) {
+        $previous_errors = NestedArray::getValue($rasterized, ['meta', 'errors']) ?: [];
+        // Add the errors to the pre-existing errors.
+        $rasterized['meta']['errors'] = array_merge($previous_errors, $normalizer_value->rasterizeValue());
+      }
+      else {
+        $rasterized['data'][] = $normalizer_value->rasterizeValue();
+      }
+    }
+    $rasterized['data'] = array_filter($rasterized['data']);
+    // Deal with the single entity case.
+    $rasterized['data'] = $this->isCollection ?
+      $rasterized['data'] :
+      reset($rasterized['data']);
+
+    // Add the self link.
+    if ($this->context['request']) {
+      /* @var \Symfony\Component\HttpFoundation\Request $request */
+      $request = $this->context['request'];
+      $rasterized['links'] = [
+        'self' => $this->linkManager->getRequestLink($request),
+      ];
+      // If this is a collection we need to append the pager data.
+      if ($this->isCollection) {
+        // Add the pager links.
+        $rasterized['links'] += $this->linkManager->getPagerLinks($request, $this->linkContext);
+
+        // Add the pre-calculated total count to the meta section.
+        if (isset($this->context['total_count'])) {
+          $rasterized['meta']['count'] = $this->context['total_count'];
+        }
+      }
+    }
+    return $rasterized;
+  }
+
+  /**
+   * Gets a flattened list of includes in all the chain.
+   *
+   * @return \Drupal\jsonapi\Normalizer\Value\EntityNormalizerValue[]
+   *   The array of included relationships.
+   */
+  public function getIncludes() {
+    $nested_includes = array_map(function ($include) {
+      return $include->getIncludes();
+    }, $this->includes);
+    $includes = array_reduce(array_filter($nested_includes), function ($carry, $item) {
+      return array_merge($carry, $item);
+    }, $this->includes);
+    // Make sure we don't output duplicate includes.
+    return array_values(array_reduce($includes, function ($unique_includes, $include) {
+      $rasterized_include = $include->rasterizeValue();
+
+      $unique_key = $rasterized_include['data'] === FALSE ?
+        $rasterized_include['meta']['errors'][0]['detail'] :
+        $rasterized_include['data']['type'] . ':' . $rasterized_include['data']['id'];
+      $unique_includes[$unique_key] = $include;
+      return $unique_includes;
+    }, []));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function rasterizeIncludes() {
+    // First gather all the includes in the chain.
+    return array_map(function ($include) {
+      return $include->rasterizeValue();
+    }, $this->getIncludes());
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Normalizer/Value/NullFieldNormalizerValue.php b/drupal/modules/jsonapi/src/Normalizer/Value/NullFieldNormalizerValue.php
new file mode 100644
index 0000000..5f20815
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Normalizer/Value/NullFieldNormalizerValue.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace Drupal\jsonapi\Normalizer\Value;
+
+use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
+
+/**
+ * @internal
+ */
+class NullFieldNormalizerValue implements FieldNormalizerValueInterface {
+
+  use RefinableCacheableDependencyTrait;
+
+  /**
+   * The property type.
+   *
+   * @var mixed
+   */
+  protected $propertyType;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getIncludes() {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPropertyType() {
+    return $this->propertyType;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setPropertyType($property_type) {
+    $this->propertyType = $property_type;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function rasterizeValue() {
+    return NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function rasterizeIncludes() {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setIncludes($includes) {
+    // Do nothing.
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getAllIncludes() {
+    return NULL;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Normalizer/Value/RelationshipItemNormalizerValue.php b/drupal/modules/jsonapi/src/Normalizer/Value/RelationshipItemNormalizerValue.php
new file mode 100644
index 0000000..7a60273
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Normalizer/Value/RelationshipItemNormalizerValue.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Drupal\jsonapi\Normalizer\Value;
+
+use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
+use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
+
+/**
+ * @internal
+ */
+class RelationshipItemNormalizerValue extends FieldItemNormalizerValue implements RefinableCacheableDependencyInterface {
+
+  use RefinableCacheableDependencyTrait;
+
+  /**
+   * Resource path.
+   *
+   * @param string
+   */
+  protected $resource;
+
+  /**
+   * Instantiates a EntityReferenceItemNormalizerValue object.
+   *
+   * @param array $values
+   *   The values.
+   * @param string $resource
+   *   The resource type of the target entity.
+   */
+  public function __construct(array $values, $resource) {
+    parent::__construct($values);
+    $this->resource = $resource;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function rasterizeValue() {
+    if (!$value = parent::rasterizeValue()) {
+      return $value;
+    }
+    $rasterized_value = [
+      'type' => $this->resource->getTypeName(),
+      'id' => empty($value['target_uuid']) ? $value : $value['target_uuid'],
+    ];
+
+    if (!empty($value['meta'])) {
+      $rasterized_value['meta'] = $value['meta'];
+    }
+
+    return $rasterized_value;
+  }
+
+  /**
+   * Sets the resource.
+   *
+   * @param string $resource
+   *   The resource to set.
+   */
+  public function setResource($resource) {
+    $this->resource = $resource;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Normalizer/Value/RelationshipNormalizerValue.php b/drupal/modules/jsonapi/src/Normalizer/Value/RelationshipNormalizerValue.php
new file mode 100644
index 0000000..5719975
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Normalizer/Value/RelationshipNormalizerValue.php
@@ -0,0 +1,95 @@
+<?php
+
+namespace Drupal\jsonapi\Normalizer\Value;
+
+/**
+ * @internal
+ */
+class RelationshipNormalizerValue extends FieldNormalizerValue {
+
+  /**
+   * The link manager.
+   *
+   * @param \Drupal\jsonapi\LinkManager\LinkManager
+   */
+  protected $linkManager;
+
+  /**
+   * The JSON API resource type.
+   *
+   * @var \Drupal\jsonapi\ResourceType\ResourceType
+   */
+  protected $resourceType;
+
+  /**
+   * The field name for the link generation.
+   *
+   * @var string
+   */
+  protected $fieldName;
+
+  /**
+   * The entity ID for the host entity.
+   *
+   * @var string
+   */
+  protected $hostEntityId;
+
+  /**
+   * Instantiate a EntityReferenceNormalizerValue object.
+   *
+   * @param RelationshipItemNormalizerValue[] $values
+   *   The normalized result.
+   * @param int $cardinality
+   *   The number of fields for the field list.
+   * @param array $link_context
+   *   All the objects and variables needed to generate the links for this
+   *   relationship.
+   */
+  public function __construct(array $values, $cardinality, array $link_context) {
+    $this->hostEntityId = $link_context['host_entity_id'];
+    $this->fieldName = $link_context['field_name'];
+    $this->linkManager = $link_context['link_manager'];
+    $this->resourceType = $link_context['resource_type'];
+    array_walk($values, function ($field_item_value) {
+      if (!$field_item_value instanceof RelationshipItemNormalizerValue) {
+        throw new \RuntimeException(sprintf('Unexpected normalizer item value for this %s.', get_called_class()));
+      }
+    });
+    parent::__construct($values, $cardinality);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function rasterizeValue() {
+    if (!$value = parent::rasterizeValue()) {
+      // According to the JSON API specs empty relationships are either NULL or
+      // an empty array.
+      return $this->cardinality == 1 ? ['data' => NULL] : ['data' => []];
+    }
+    // Generate the links for the relationship.
+    $route_parameters = [
+      // Make sure to point to the public facing fields.
+      'related' => $this->resourceType->getPublicName($this->fieldName),
+    ];
+    return [
+      'data' => $value,
+      'links' => [
+        'self' => $this->linkManager->getEntityLink(
+          $this->hostEntityId,
+          $this->resourceType,
+          $route_parameters,
+          'relationship'
+        ),
+        'related' => $this->linkManager->getEntityLink(
+          $this->hostEntityId,
+          $this->resourceType,
+          $route_parameters,
+          'related'
+        ),
+      ],
+    ];
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Normalizer/Value/ValueExtractorInterface.php b/drupal/modules/jsonapi/src/Normalizer/Value/ValueExtractorInterface.php
new file mode 100644
index 0000000..a65a218
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Normalizer/Value/ValueExtractorInterface.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Drupal\jsonapi\Normalizer\Value;
+
+/**
+ * @internal
+ */
+interface ValueExtractorInterface {
+
+  /**
+   * Get the rasterized value.
+   *
+   * @return mixed
+   *   The value.
+   */
+  public function rasterizeValue();
+
+  /**
+   * Get the includes.
+   *
+   * @return array[]
+   *   An array of includes keyed by entity type and id pair.
+   */
+  public function rasterizeIncludes();
+
+}
diff --git a/drupal/modules/jsonapi/src/ParamConverter/EntityUuidConverter.php b/drupal/modules/jsonapi/src/ParamConverter/EntityUuidConverter.php
new file mode 100644
index 0000000..d721df6
--- /dev/null
+++ b/drupal/modules/jsonapi/src/ParamConverter/EntityUuidConverter.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Drupal\jsonapi\ParamConverter;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\ParamConverter\EntityConverter;
+use Drupal\Core\TypedData\TranslatableInterface;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Parameter converter for upcasting entity UUIDs to full objects.
+ *
+ * @see \Drupal\Core\ParamConverter\EntityConverter
+ *
+ * @todo Remove when https://www.drupal.org/node/2353611 lands.
+ *
+ * @internal
+ */
+class EntityUuidConverter extends EntityConverter {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function convert($value, $definition, $name, array $defaults) {
+    $entity_type_id = $this->getEntityTypeFromDefaults($definition, $name, $defaults);
+    if ($storage = $this->entityManager->getStorage($entity_type_id)) {
+      if (!$entities = $storage->loadByProperties(['uuid' => $value])) {
+        return NULL;
+      }
+      $entity = reset($entities);
+      // If the entity type is translatable, ensure we return the proper
+      // translation object for the current context.
+      if ($entity instanceof EntityInterface && $entity instanceof TranslatableInterface) {
+        $entity = $this->entityManager->getTranslationFromContext($entity, NULL, ['operation' => 'entity_upcast']);
+      }
+      return $entity;
+    }
+    return NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applies($definition, $name, Route $route) {
+    return $route->getOption('_is_jsonapi');
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Query/ConditionOption.php b/drupal/modules/jsonapi/src/Query/ConditionOption.php
new file mode 100644
index 0000000..05e2d32
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Query/ConditionOption.php
@@ -0,0 +1,101 @@
+<?php
+
+namespace Drupal\jsonapi\Query;
+
+/**
+ * A ConditionOption represents an option which can be applied to a query.
+ *
+ * @internal
+ */
+class ConditionOption implements QueryOptionInterface {
+
+  /**
+   * A unique key.
+   *
+   * @var string
+   */
+  protected $id;
+
+  /**
+   * A unique key representing the intended parent of this option.
+   *
+   * @var string|null
+   */
+  protected $parentId;
+
+  /**
+   * String representation of the entity field in to be checked.
+   *
+   * @var string
+   */
+  protected $field;
+
+  /**
+   * Value of the condition for the given field.
+   *
+   * @var string|string[]
+   */
+  protected $value;
+
+  /**
+   * Conditional operator with which to compare values.
+   *
+   * @var string
+   */
+  protected $operator;
+
+  /**
+   * The langcode of the field to check.
+   *
+   * @var string
+   */
+  protected $langcode;
+
+  /**
+   * Constructs a new ConditionOption.
+   *
+   * @param string $id
+   *   A unique string identifier for the option.
+   * @param string|\Drupal\jsonapi\Query\GroupOption $field
+   *   Either a field name or a GroupOption.
+   * @param mixed $value
+   *   Value for comparison.
+   * @param string $operator
+   *   Boolean operator.
+   * @param string $langcode
+   *   Language of entity to compare against.
+   */
+  public function __construct($id, $field, $value = NULL, $operator = NULL, $langcode = NULL, $parent_id = NULL) {
+    $this->id = $id;
+    $this->field = $field;
+    $this->value = $value;
+    $this->operator = $operator;
+    $this->langcode = $langcode;
+    $this->parentId = $parent_id;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function id() {
+    return $this->id;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function apply($query) {
+    return $query->condition($this->field, $this->value, $this->operator, $this->langcode);
+  }
+
+  /**
+   * Returns the id of this option's parent.
+   *
+   * @return string|null
+   *   Either the id of its parent or NULL.
+   */
+  public function parentId() {
+    return $this->parentId;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Query/GroupOption.php b/drupal/modules/jsonapi/src/Query/GroupOption.php
new file mode 100644
index 0000000..d51a20c
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Query/GroupOption.php
@@ -0,0 +1,153 @@
+<?php
+
+namespace Drupal\jsonapi\Query;
+
+/**
+ * A GroupOption can group other options before applying them to a query.
+ *
+ * @internal
+ */
+class GroupOption implements QueryOptionInterface, QueryOptionTreeItemInterface {
+
+  /**
+   * A unique key.
+   *
+   * @var string
+   */
+  protected $id;
+
+  /**
+   * A unique key representing a parent condition group.
+   *
+   * @var string
+   */
+  protected $parentGroup;
+
+  /**
+   * An array of QueryOptions.
+   *
+   * @var \Drupal\jsonapi\Query\QueryOptionInterface[]
+   */
+  protected $childOptions;
+
+  /**
+   * An array of GroupOptions.
+   *
+   * @var \Drupal\jsonapi\Query\GroupOption[]
+   */
+  protected $childGroups;
+
+  /**
+   * Conjunction of the groups conditions.
+   *
+   * @var string
+   */
+  protected $conjunction;
+
+  /**
+   * Constructs a new GroupOption.
+   *
+   * @param string $id
+   *   A unique string identifier for the option.
+   * @param string $conjunction
+   *   Conjunction of the groups conditions.
+   * @param string $parent_group
+   *   A unique key representing a parent condition group.
+   */
+  public function __construct($id, $conjunction = 'AND', $parent_group = NULL) {
+    $this->id = $id;
+    $this->conjunction = $conjunction;
+    $this->parentGroup = $parent_group;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function id() {
+    return $this->id;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function parentId() {
+    return $this->parentGroup;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function insert($target_id, QueryOptionInterface $option) {
+    $find_proper_id = function ($child_id, $group_option) use ($target_id) {
+      if ($child_id) {
+        return $child_id;
+      };
+      return $group_option->hasChild($target_id) ?
+        $group_option->id() :
+        NULL;
+    };
+
+    if ($this->id() == $target_id) {
+      $prop = $option instanceof GroupOption ? 'childGroups' : 'childOptions';
+      $this->{$prop}[$option->id()] = $option;
+      return TRUE;
+    }
+    elseif ($proper_child = array_reduce($this->childGroups, $find_proper_id, NULL)) {
+      return $this->childGroups[$proper_child]->insert($target_id, $option);
+    }
+
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function apply($query) {
+    switch ($this->conjunction) {
+      case 'OR':
+        $group = $query->orConditionGroup();
+        break;
+
+      case 'AND':
+      default:
+        $group = $query->andConditionGroup();
+        break;
+    }
+
+    if (!empty($this->childOptions)) {
+      $group = array_reduce($this->childOptions, function ($group, $child) {
+        return $child->apply($group);
+      }, $group);
+    }
+
+    if (!empty($this->childGroups)) {
+      $group = array_reduce($this->childGroups, function ($group, $child) {
+        return $child->apply($group);
+      }, $group);
+    }
+
+    return $query->condition($group);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasChild($id) {
+    // Return FALSE if this node has no child.
+    if (!isset($this->childOptions) || empty($this->childOptions)) {
+      return FALSE;
+    }
+
+    // If any of the options have the specified id, return TRUE.
+    if (in_array($id, array_keys($this->childOptions))) {
+      return TRUE;
+    }
+
+    // If any child GroupOptions or their children have the id return TRUE.
+    return array_reduce($this->groupOptions, function ($has_child, $group) use ($id) {
+      // If we already know that we have the child, skip evaluation and return.
+      return $has_child || $group->id() == $id || $group->hasChild($id);
+    }, FALSE);
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Query/OffsetPagerOption.php b/drupal/modules/jsonapi/src/Query/OffsetPagerOption.php
new file mode 100644
index 0000000..9e08cf3
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Query/OffsetPagerOption.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace Drupal\jsonapi\Query;
+
+/**
+ * @internal
+ */
+class OffsetPagerOption implements QueryOptionInterface {
+
+  /**
+   * The size.
+   *
+   * @var int
+   */
+  protected $size;
+
+  /**
+   * The offset.
+   *
+   * @var int
+   */
+  protected $offset;
+
+  /**
+   * Creates a PagerOption object.
+   *
+   * @param int $size
+   *   The maximum number of items to return.
+   * @param int $offset
+   *   The starting element.
+   */
+  public function __construct($size, $offset = 0) {
+    $this->size = $size;
+    $this->offset = $offset ?: 0;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function id() {
+    return 'offset_pager';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function apply($query) {
+    if (isset($this->offset) && isset($this->size)) {
+      // Request one extra entity to know if there is a next page.
+      $query->range($this->offset, $this->size + 1);
+      $query->addMetaData('pager_size', (int) $this->size);
+    }
+
+    return $query;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Query/QueryBuilder.php b/drupal/modules/jsonapi/src/Query/QueryBuilder.php
new file mode 100644
index 0000000..6a01b36
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Query/QueryBuilder.php
@@ -0,0 +1,362 @@
+<?php
+
+namespace Drupal\jsonapi\Query;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\jsonapi\Routing\Param\OffsetPage;
+use Drupal\jsonapi\Routing\Param\Filter;
+use Drupal\jsonapi\Routing\Param\JsonApiParamInterface;
+use Drupal\jsonapi\Context\CurrentContext;
+use Drupal\jsonapi\Context\FieldResolver;
+use Drupal\jsonapi\Routing\Param\Sort;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+
+/**
+ * @internal
+ */
+class QueryBuilder {
+
+  /**
+   * The entity type object that should be used for the query.
+   */
+  protected $entityType;
+
+  /**
+   * The options to build with which to build a query.
+   */
+  protected $options = [];
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The JSON API current context service.
+   *
+   * @var \Drupal\jsonapi\Context\CurrentContext
+   */
+  protected $currentContext;
+
+  /**
+   * The field resolver service.
+   *
+   * @var \Drupal\jsonapi\Context\FieldResolver
+   */
+  protected $fieldResolver;
+
+  /**
+   * Contructs a new QueryBuilder object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   An instance of a QueryFactory.
+   * @param \Drupal\jsonapi\Context\CurrentContext $current_context
+   *   An instance of the current context service.
+   * @param \Drupal\jsonapi\Context\FieldResolver $field_resolver
+   *   The field resolver service.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, CurrentContext $current_context, FieldResolver $field_resolver) {
+    $this->entityTypeManager = $entity_type_manager;
+    $this->currentContext = $current_context;
+    $this->fieldResolver = $field_resolver;
+  }
+
+  /**
+   * Creates a new Entity Query.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type for which to create a query.
+   * @param \Drupal\jsonapi\Routing\Param\JsonApiParamInterface[] $params
+   *   The JSON API parameters.
+   *
+   * @return \Drupal\Core\Entity\Query\QueryInterface
+   *   The new query.
+   */
+  public function newQuery(EntityTypeInterface $entity_type, array $params = []) {
+    $this->entityType = $entity_type;
+    $this->options = [];
+    $this->configureFromContext($params);
+
+    $query = $this->entityTypeManager
+      ->getStorage($this->entityType->id())
+      ->getQuery()
+      ->accessCheck(TRUE);
+
+    // This applies each option from the option tree to the query before
+    // returning it.
+    $applied_query = array_reduce($this->options, function ($query, $option) {
+      /* @var \Drupal\jsonapi\Query\QueryOptionInterface $option */
+      return $option->apply($query);
+    }, $query);
+
+    return $applied_query ? $applied_query : $query;
+  }
+
+  /**
+   * Configure the query from the current context and the provided parameters.
+   *
+   * To avoid using the global context so much use the passed in parameters
+   * over the ones in the current context.
+   *
+   * @param \Drupal\jsonapi\Routing\Param\JsonApiParamInterface[] $params
+   *   The JSON API parameters.
+   */
+  protected function configureFromContext(array $params = []) {
+    // TODO: Explore the possibility to turn JsonApiParam into a plugin type.
+    $param_keys = [Filter::KEY_NAME, Sort::KEY_NAME];
+    foreach ($param_keys as $param_key) {
+      if (isset($params[$param_key])) {
+        $this->configureParam($param_key, $params[$param_key]);
+      }
+      elseif ($param = $this->currentContext->getJsonApiParameter($param_key)) {
+        $this->configureParam($param_key, $param);
+      }
+    }
+    // We always add a default pagination parameter.
+    $pager = isset($params[OffsetPage::KEY_NAME]) ?
+      $params[OffsetPage::KEY_NAME] :
+      new OffsetPage([]);
+    $this->configureParam(OffsetPage::KEY_NAME, $pager);
+  }
+
+  /**
+   * Configure a parameter based on the type parameter type.
+   *
+   * @param string $type
+   *   The parameter type.
+   * @param \Drupal\jsonapi\Routing\Param\JsonApiParamInterface $param
+   *   The parameter to configure.
+   */
+  protected function configureParam($type, JsonApiParamInterface $param) {
+    switch ($type) {
+      case Filter::KEY_NAME:
+        $this->configureFilter($param);
+        break;
+
+      case Sort::KEY_NAME:
+        $this->configureSort($param);
+        break;
+
+      case OffsetPage::KEY_NAME:
+        $this->configurePager($param);
+        break;
+    }
+  }
+
+  /**
+   * Configures the query builder from a Filter parameter.
+   *
+   * @param \Drupal\jsonapi\Routing\Param\JsonApiParamInterface $param
+   *   A Filter parameter from which to configure this query builder.
+   *
+   * @todo The nested closures passing parameters by reference may not be ideal.
+   */
+  protected function configureFilter(JsonApiParamInterface $param) {
+    $extracted = [];
+
+    foreach ($param->get() as $filter_index => $filter) {
+      foreach ($filter as $filter_type => $properties) {
+        switch ($filter_type) {
+          case Filter::CONDITION_KEY:
+            $extracted[] = $this->newCondtionOption($filter_index, $properties);
+            break;
+
+          case Filter::GROUP_KEY:
+            $extracted[] = $this->newGroupOption($filter_index, $properties);
+            break;
+
+          default:
+            throw new BadRequestHttpException(
+              sprintf('Invalid syntax in the filter parameter: %s.', $filter_index)
+            );
+        };
+      }
+    }
+
+    $this->buildTree($extracted);
+  }
+
+  /**
+   * Configures the query builder from a Sort parameter.
+   *
+   * @param \Drupal\jsonapi\Routing\Param\JsonApiParamInterface $param
+   *   A Sort parameter from which to configure this query builder.
+   */
+  protected function configureSort(JsonApiParamInterface $param) {
+    $extracted = [];
+    foreach ($param->get() as $sort_index => $sort) {
+      $extracted[] = $this->newSortOption(sprintf('sort_%s', $sort_index), $sort);
+    }
+
+    $this->buildTree($extracted);
+  }
+
+  /**
+   * Configures the query builder from a Pager parameter.
+   *
+   * @param \Drupal\jsonapi\Routing\Param\JsonApiParamInterface $param
+   *   A pager parameter from which to configure this query builder.
+   */
+  protected function configurePager(JsonApiParamInterface $param) {
+    $this->buildTree([$this->newPagerOption($param->get())]);
+  }
+
+  /**
+   * Returns a new ConditionOption.
+   *
+   * @param string $condition_id
+   *   A unique id for the option.
+   * @param array $properties
+   *   The condition properties.
+   *
+   * @return \Drupal\jsonapi\Query\ConditionOption
+   *   The condition object.
+   */
+  protected function newCondtionOption($condition_id, array $properties) {
+    $langcode_key = $this->getLangcodeKey();
+    $langcode = isset($properties[$langcode_key]) ? $properties[$langcode_key] : NULL;
+    $membership = isset($properties[Filter::MEMBER_KEY]) ? $properties[Filter::MEMBER_KEY] : NULL;
+    $field = isset($properties[Filter::PATH_KEY]) ? $properties[Filter::PATH_KEY] : NULL;
+    $value = isset($properties[Filter::VALUE_KEY]) ? $properties[Filter::VALUE_KEY] : NULL;
+    $operator = isset($properties[Filter::OPERATOR_KEY]) ? $properties[Filter::OPERATOR_KEY] : NULL;
+    return new ConditionOption(
+      $condition_id,
+      $this->fieldResolver->resolveInternal($field),
+      $value,
+      $operator,
+      $langcode,
+      $membership
+    );
+  }
+
+  /**
+   * Returns a new GroupOption.
+   *
+   * @param string $identifier
+   *   A unique id for the option.
+   * @param array $properties
+   *   The group properties.
+   *
+   * @return \Drupal\jsonapi\Query\GroupOption
+   *   The group object.
+   */
+  protected function newGroupOption($identifier, array $properties) {
+    $parent_group = isset($properties[Filter::MEMBER_KEY]) ? $properties[Filter::MEMBER_KEY] : NULL;
+    $conjunction = isset($properties[Filter::CONJUNCTION_KEY]) ? $properties[Filter::CONJUNCTION_KEY] : NULL;
+    return new GroupOption($identifier, $conjunction, $parent_group);
+  }
+
+  /**
+   * Returns a new SortOption.
+   *
+   * @param string $identifier
+   *   A unique id for the option.
+   * @param array $properties
+   *   The sort properties.
+   *
+   * @return \Drupal\jsonapi\Query\SortOption
+   *   The sort object.
+   */
+  protected function newSortOption($identifier, array $properties) {
+    $field = isset($properties[Sort::FIELD_KEY]) ? $properties[Sort::FIELD_KEY] : NULL;
+    $direction = isset($properties[Sort::DIRECTION_KEY]) ? $properties[Sort::DIRECTION_KEY] : NULL;
+    $langcode = isset($properties[Sort::LANGUAGE_KEY]) ? $properties[Sort::LANGUAGE_KEY] : NULL;
+    return new SortOption(
+      $identifier,
+      $this->fieldResolver->resolveInternal($field),
+      $direction,
+      $langcode
+    );
+  }
+
+  /**
+   * Returns a new SortOption.
+   *
+   * @param array $properties
+   *   The pager properties.
+   *
+   * @return \Drupal\jsonapi\Query\OffsetPagerOption
+   *   The sort object.
+   */
+  protected function newPagerOption(array $properties) {
+    // Add defaults to avoid unset warnings.
+    $properties += [
+      'limit' => NULL,
+      'offset' => 0,
+    ];
+    return new OffsetPagerOption($properties['limit'], $properties['offset']);
+  }
+
+  /**
+   * Builds a tree of QueryOptions.
+   *
+   * @param \Drupal\jsonapi\Query\QueryOptionInterface[] $options
+   *   An array of QueryOptions.
+   */
+  protected function buildTree(array $options) {
+    $remaining = $options;
+    while (!empty($remaining)) {
+      $insert = array_pop($remaining);
+      if (method_exists($insert, 'parentId') && $parent_id = $insert->parentId()) {
+        if (!$this->insert($parent_id, $insert)) {
+          array_unshift($remaining, $insert);
+        }
+      }
+      else {
+        $this->options[$insert->id()] = $insert;
+      }
+    }
+  }
+
+  /**
+   * Inserts a QueryOption into the appropriate child QueryOption.
+   *
+   * @param string $target_id
+   *   Unique ID of the intended QueryOption parent.
+   * @param \Drupal\jsonapi\Query\QueryOptionInterface $option
+   *   The QueryOption to insert.
+   *
+   * @return bool
+   *   Whether the option could be inserted or not.
+   */
+  protected function insert($target_id, QueryOptionInterface $option) {
+    if (!empty($this->options)) {
+      $find_target_child = function ($child, QueryOptionInterface $my_option) use ($target_id) {
+        if ($child) {
+          return $child;
+        }
+        if (
+          $my_option->id() == $target_id ||
+          (method_exists($my_option, 'hasChild') && $my_option->hasChild($target_id))
+        ) {
+          return $my_option->id();
+        }
+        return FALSE;
+      };
+
+      if ($appropriate_child = array_reduce($this->options, $find_target_child, NULL)) {
+        return $this->options[$appropriate_child]->insert($target_id, $option);
+      }
+    }
+
+    return FALSE;
+  }
+
+  /**
+   * Get the language code key.
+   *
+   * @return string
+   *   The key.
+   */
+  protected function getLangcodeKey() {
+    $entity_type_id = $this->currentContext->getResourceType()
+      ->getEntityTypeId();
+    return $this->entityTypeManager
+      ->getDefinition($entity_type_id)
+      ->getKey('langcode');
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Query/QueryOptionInterface.php b/drupal/modules/jsonapi/src/Query/QueryOptionInterface.php
new file mode 100644
index 0000000..91110f0
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Query/QueryOptionInterface.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Drupal\jsonapi\Query;
+
+/**
+ * @internal
+ */
+interface QueryOptionInterface {
+
+  /**
+   * Returns a unique id for this query.
+   *
+   * @return string
+   *   The ID for the query.
+   */
+  public function id();
+
+  /**
+   * Receives a QueryInterface and applies the current QueryOption to it.
+   *
+   * @param \Drupal\Core\Entity\Query\QueryInterface|\Drupal\Core\Entity\Query\ConditionInterface $query
+   *   A query or condition group to which this option should be applied.
+   *
+   * @return \Drupal\Core\Entity\Query\QueryInterface|\Drupal\Core\Entity\Query\ConditionInterface
+   *   A query or condition with the current option applied to it.
+   */
+  public function apply($query);
+
+}
diff --git a/drupal/modules/jsonapi/src/Query/QueryOptionTreeItemInterface.php b/drupal/modules/jsonapi/src/Query/QueryOptionTreeItemInterface.php
new file mode 100644
index 0000000..102921c
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Query/QueryOptionTreeItemInterface.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Drupal\jsonapi\Query;
+
+/**
+ * @internal
+ */
+interface QueryOptionTreeItemInterface {
+
+  /**
+   * Insert the child into this object or one if its children objects.
+   *
+   * @param string $target_id
+   *   The QueryOption id of the intended parent.
+   * @param \Drupal\jsonapi\Query\QueryOptionInterface $option
+   *   The QueryOption to insert.
+   *
+   * @return bool
+   *   Whether or not the QueryOption could be inserted.
+   */
+  public function insert($target_id, QueryOptionInterface $option);
+
+  /**
+   * Returns whether or the given id is a (grand)child of the object.
+   */
+  public function hasChild($id);
+
+}
diff --git a/drupal/modules/jsonapi/src/Query/SortOption.php b/drupal/modules/jsonapi/src/Query/SortOption.php
new file mode 100644
index 0000000..54901f8
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Query/SortOption.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Drupal\jsonapi\Query;
+
+/**
+ * @internal
+ */
+class SortOption implements QueryOptionInterface {
+
+  /**
+   * A unique key.
+   *
+   * @var string
+   */
+  protected $id;
+
+  /**
+   * The field by which to sort.
+   *
+   * @var string
+   */
+  protected $field;
+
+  /**
+   * The direction of the sort.
+   *
+   * @var string
+   */
+  protected $direction;
+
+  /**
+   * The langcode for the sort.
+   *
+   * @var string
+   */
+  protected $langcode;
+
+  /**
+   * Creates a SortOption object.
+   *
+   * @param string $id
+   *   An identifier for the sort options.
+   * @param string $field
+   *   The field by which to sort.
+   * @param string $field
+   *   The direction for the sort.
+   * @param string $langcode
+   *   The language variant of the field to sort by.
+   */
+  public function __construct($id, $field, $direction = 'ASC', $langcode = NULL) {
+    $this->id = $id;
+    $this->field = $field;
+    $this->direction = $direction;
+    $this->langcode = $langcode;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function id() {
+    return $this->id;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function apply($query) {
+    return $query->sort($this->field, $this->direction);
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Resource/EntityCollection.php b/drupal/modules/jsonapi/src/Resource/EntityCollection.php
new file mode 100644
index 0000000..8d23708
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Resource/EntityCollection.php
@@ -0,0 +1,110 @@
+<?php
+
+namespace Drupal\jsonapi\Resource;
+
+/**
+ * Wrapper to normalize collections with multiple entities.
+ *
+ * @internal
+ */
+class EntityCollection implements \IteratorAggregate, \Countable {
+
+  /**
+   * Entity storage.
+   *
+   * @var \Drupal\Core\Entity\EntityInterface[]
+   */
+  protected $entities;
+
+  /**
+   * Holds a boolean indicating if there is a next page.
+   *
+   * @var bool
+   */
+  protected $hasNextPage;
+
+  /**
+   * Holds the total count of entities.
+   *
+   * @var int
+   */
+  protected $count;
+
+  /**
+   * Instantiates a EntityCollection object.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface[] $entities
+   *   The entities for the collection.
+   */
+  public function __construct(array $entities) {
+    $this->entities = array_filter(array_values($entities));
+  }
+
+  /**
+   * Returns an iterator for entities.
+   *
+   * @return \ArrayIterator
+   *   An \ArrayIterator instance
+   */
+  public function getIterator() {
+    return new \ArrayIterator($this->entities);
+  }
+
+  /**
+   * Returns the number of entities.
+   *
+   * @return int
+   *   The number of parameters
+   */
+  public function count() {
+    return count($this->entities);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTotalCount() {
+    return $this->count;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setTotalCount($count) {
+    $this->count = $count;
+  }
+
+  /**
+   * Returns the collection as an array.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface[]
+   *   The array of entities.
+   */
+  public function toArray() {
+    return $this->entities;
+  }
+
+  /**
+   * Checks if there is a next page in the collection.
+   *
+   * @return bool
+   *   TRUE if the collection has a next page.
+   */
+  public function hasNextPage() {
+    return (bool) $this->hasNextPage;
+  }
+
+  /**
+   * Sets the has next page flag.
+   *
+   * Once the collection query has been executed and we build the entity collection, we now if there will be a next page
+   * with extra entities.
+   *
+   * @param bool $has_next_page
+   *   TRUE if the collection has a next page.
+   */
+  public function setHasNextPage($has_next_page) {
+    $this->hasNextPage = (bool) $has_next_page;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Resource/JsonApiDocumentTopLevel.php b/drupal/modules/jsonapi/src/Resource/JsonApiDocumentTopLevel.php
new file mode 100644
index 0000000..ccecd88
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Resource/JsonApiDocumentTopLevel.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Drupal\jsonapi\Resource;
+
+/**
+ * Represents a JSON API document's "top level".
+ *
+ * @see http://jsonapi.org/format/#document-top-level
+ *
+ * @internal
+ *
+ * @todo Add the missing required members: 'error' and 'meta' or document why not.
+ * @todo Add support for the missing optional members: 'jsonapi', 'links' and 'included' or document why not.
+ */
+class JsonApiDocumentTopLevel {
+
+  /**
+   * The data to normalize.
+   *
+   * @var \Drupal\Core\Entity\EntityInterface|\Drupal\jsonapi\EntityCollection
+   */
+  protected $data;
+
+  /**
+   * Instantiates a JsonApiDocumentTopLevel object.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface|\Drupal\jsonapi\EntityCollection $data
+   *   The data to normalize. It can be either a straight up entity or a
+   *   collection of entities.
+   */
+  public function __construct($data) {
+    $this->data = $data;
+  }
+
+  /**
+   * Gets the data.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface|\Drupal\jsonapi\EntityCollection
+   *   The data.
+   */
+  public function getData() {
+    return $this->data;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/ResourceResponse.php b/drupal/modules/jsonapi/src/ResourceResponse.php
new file mode 100644
index 0000000..8820440
--- /dev/null
+++ b/drupal/modules/jsonapi/src/ResourceResponse.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace Drupal\jsonapi;
+
+use Drupal\Core\Cache\CacheableResponseInterface;
+use Drupal\Core\Cache\CacheableResponseTrait;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Contains data for serialization before sending the response.
+ *
+ * We do not want to abuse the $content property on the Response class to store
+ * our response data. $content implies that the provided data must either be a
+ * string or an object with a __toString() method, which is not a requirement
+ * for data used here.
+ *
+ * @see \Drupal\rest\ModifiedResourceResponse
+ *
+ * @internal
+ */
+class ResourceResponse extends Response implements CacheableResponseInterface {
+
+  use CacheableResponseTrait;
+
+  /**
+   * Response data that should be serialized.
+   *
+   * @var mixed
+   */
+  protected $responseData;
+
+  /**
+   * Constructor for ResourceResponse objects.
+   *
+   * @param mixed $data
+   *   Response data that should be serialized.
+   * @param int $status
+   *   The response status code.
+   * @param array $headers
+   *   An array of response headers.
+   */
+  public function __construct($data = NULL, $status = 200, $headers = []) {
+    $this->responseData = $data;
+    parent::__construct('', $status, $headers);
+  }
+
+  /**
+   * Returns response data that should be serialized.
+   *
+   * @return mixed
+   *   Response data that should be serialized.
+   */
+  public function getResponseData() {
+    return $this->responseData;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/ResourceType/ResourceType.php b/drupal/modules/jsonapi/src/ResourceType/ResourceType.php
new file mode 100644
index 0000000..c16ea05
--- /dev/null
+++ b/drupal/modules/jsonapi/src/ResourceType/ResourceType.php
@@ -0,0 +1,165 @@
+<?php
+
+namespace Drupal\jsonapi\ResourceType;
+
+/**
+ * Value object containing all metadata for a JSON API resource type.
+ *
+ * Used to generate routes (collection, individual, etcetera), generate
+ * relationship links, and so on.
+ *
+ * @see \Drupal\jsonapi\ResourceType\ResourceTypeRepository
+ *
+ * @internal
+ */
+class ResourceType {
+
+  /**
+   * The entity type ID.
+   *
+   * @var string
+   */
+  protected $entityTypeId;
+
+  /**
+   * The bundle ID.
+   *
+   * @var string
+   */
+  protected $bundle;
+
+  /**
+   * The type name.
+   *
+   * @var string
+   */
+  protected $typeName;
+
+  /**
+   * The class to which a payload converts to.
+   *
+   * @var string
+   */
+  protected $deserializationTargetClass;
+
+  /**
+   * Gets the entity type ID.
+   *
+   * @return string
+   *   The entity type ID.
+   *
+   * @see \Drupal\Core\Entity\EntityInterface::getEntityTypeId
+   */
+  public function getEntityTypeId() {
+    return $this->entityTypeId;
+  }
+
+  /**
+   * Gets the type name.
+   *
+   * @return string
+   *   The type name.
+   */
+  public function getTypeName() {
+    return $this->typeName;
+  }
+
+  /**
+   * Gets the bundle.
+   *
+   * @return string
+   *   The bundle of the entity. Defaults to the entity type ID if the entity
+   *   type does not make use of different bundles.
+   *
+   * @see \Drupal\Core\Entity\EntityInterface::bundle
+   */
+  public function getBundle() {
+    return $this->bundle;
+  }
+
+  /**
+   * Gets the deserialization target class.
+   *
+   * @return string
+   *   The deserialization target class.
+   */
+  public function getDeserializationTargetClass() {
+    return $this->deserializationTargetClass;
+  }
+
+  /**
+   * Translates the entity field name to the public field name.
+   *
+   * This is only here so we can allow polymorphic implementations to take a
+   * greater control on the field names.
+   *
+   * @return string
+   *   The public field name.
+   */
+  public function getPublicName($field_name) {
+    // By default the entity field name is the public field name.
+    return $field_name;
+  }
+
+  /**
+   * Translates the public field name to the entity field name.
+   *
+   * This is only here so we can allow polymorphic implementations to take a
+   * greater control on the field names.
+   *
+   * @return string
+   *   The internal field name as defined in the entity.
+   */
+  public function getInternalName($field_name) {
+    // By default the entity field name is the public field name.
+    return $field_name;
+  }
+
+  /**
+   * Checks if a field is enabled or not.
+   *
+   * This is only here so we can allow polymorphic implementations to take a
+   * greater control on the data model.
+   *
+   * @param string $field_name
+   *   The internal field name.
+   *
+   * @return bool
+   *   TRUE if the field is enabled and should be considered as part of the data
+   *   model. FALSE otherwise.
+   */
+  public function isFieldEnabled($field_name) {
+    // By default all fields are enabled.
+    return TRUE;
+  }
+
+  /**
+   * Determine whether to include a collection count.
+   *
+   * @return bool
+   *   Whether to include a collection count.
+   */
+  public function includeCount() {
+    // By default, do not return counts in collection queries.
+    return FALSE;
+  }
+
+  /**
+   * Instantiates a ResourceType object.
+   *
+   * @param string $entity_type_id
+   *   An entity type ID.
+   * @param string $bundle
+   *   A bundle.
+   * @param string $deserialization_target_class
+   *   The deserialization target class.
+   */
+  public function __construct($entity_type_id, $bundle, $deserialization_target_class) {
+    $this->entityTypeId = $entity_type_id;
+    $this->bundle = $bundle;
+    $this->deserializationTargetClass = $deserialization_target_class;
+
+    $this->typeName = sprintf('%s--%s', $this->entityTypeId, $this->bundle);
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php b/drupal/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php
new file mode 100644
index 0000000..cb3a421
--- /dev/null
+++ b/drupal/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php
@@ -0,0 +1,124 @@
+<?php
+
+namespace Drupal\jsonapi\ResourceType;
+
+use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException;
+
+/**
+ * Provides a repository of all JSON API resource types.
+ *
+ * Contains the complete set of ResourceType value objects, which are auto-
+ * generated based on the Entity Type Manager and Entity Type Bundle Info: one
+ * JSON API resource type per entity type bundle. So, for example:
+ * - node--article
+ * - node--page
+ * - node--…
+ * - user--user
+ * - …
+ *
+ * @see \Drupal\jsonapi\ResourceType\ResourceType
+ *
+ * @internal
+ */
+class ResourceTypeRepository {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The bundle manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
+   */
+  protected $bundleManager;
+
+  /**
+   * All JSON API resource types.
+   *
+   * @var \Drupal\jsonapi\ResourceType\ResourceType[]
+   */
+  protected $all = [];
+
+  /**
+   * Instantiates a ResourceTypeRepository object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_manager
+   *   The bundle manager.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $bundle_manager) {
+    $this->entityTypeManager = $entity_type_manager;
+    $this->bundleManager = $bundle_manager;
+  }
+
+  /**
+   * Gets all JSON API resource types.
+   *
+   * @return \Drupal\jsonapi\ResourceType\ResourceType[]
+   *   The set of all JSON API resource types in this Drupal instance.
+   */
+  public function all() {
+    if (!$this->all) {
+      $entity_type_ids = array_keys($this->entityTypeManager->getDefinitions());
+      foreach ($entity_type_ids as $entity_type_id) {
+        $this->all = array_merge($this->all, array_map(function ($bundle) use ($entity_type_id) {
+          return new ResourceType(
+            $entity_type_id,
+            $bundle,
+            $this->entityTypeManager->getDefinition($entity_type_id)->getClass()
+          );
+        }, array_keys($this->bundleManager->getBundleInfo($entity_type_id))));
+      }
+    }
+    return $this->all;
+  }
+
+  /**
+   * Gets a specific JSON API resource type based on entity type ID and bundle.
+   *
+   * @param string $entity_type_id
+   *   The entity type id.
+   * @param string $bundle_id
+   *   The id for the bundle to find.
+   *
+   * @return \Drupal\jsonapi\ResourceType\ResourceType
+   *   The requested JSON API resource type, if it exists. NULL otherwise.
+   */
+  public function get($entity_type_id, $bundle) {
+    if (empty($entity_type_id)) {
+      throw new PreconditionFailedHttpException('Server error. The current route is malformed.');
+    }
+    foreach ($this->all() as $resource) {
+      if ($resource->getEntityTypeId() == $entity_type_id && $resource->getBundle() == $bundle) {
+        return $resource;
+      }
+    }
+    return NULL;
+  }
+
+  /**
+   * Gets a specific JSON API resource type based on a supplied typename.
+   *
+   * @param string $type_name
+   *   The public typename of a JSON API resource.
+   *
+   * @return \Drupal\jsonapi\ResourceType\ResourceType|null
+   *   The resource type, or NULL if none found.
+   */
+  public function getByTypeName($type_name) {
+    foreach ($this->all() as $resource) {
+      if ($resource->getTypeName() == $type_name) {
+        return $resource;
+      }
+    }
+    return NULL;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Routing/JsonApiParamEnhancer.php b/drupal/modules/jsonapi/src/Routing/JsonApiParamEnhancer.php
new file mode 100644
index 0000000..b0274d9
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Routing/JsonApiParamEnhancer.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace Drupal\jsonapi\Routing;
+
+use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Routing\Enhancer\RouteEnhancerInterface;
+use Drupal\jsonapi\Routing\Param\OffsetPage;
+use Drupal\jsonapi\Routing\Param\Filter;
+use Drupal\jsonapi\Routing\Param\Sort;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Route;
+
+/**
+ * @internal
+ */
+class JsonApiParamEnhancer implements RouteEnhancerInterface {
+
+  /**
+   * The field manager.
+   *
+   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
+   */
+  protected $fieldManager;
+
+  /**
+   * Instantiates a JsonApiParamEnhancer object.
+   *
+   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $field_manager
+   *   The field manager.
+   */
+  public function __construct(EntityFieldManagerInterface $field_manager) {
+    $this->fieldManager = $field_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applies(Route $route) {
+    // This enhancer applies to the JSON API routes.
+    return $route->getDefault(RouteObjectInterface::CONTROLLER_NAME) == Routes::FRONT_CONTROLLER;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function enhance(array $defaults, Request $request) {
+    $options = [];
+    if ($request->query->has('filter')) {
+      $entity_type_id = $defaults[RouteObjectInterface::ROUTE_OBJECT]->getRequirement('_entity_type');
+      $options['filter'] = new Filter($request->query->get('filter'), $entity_type_id, $this->fieldManager);
+    }
+    if ($request->query->has('sort')) {
+      $options['sort'] = new Sort($request->query->get('sort'));
+    }
+    if ($request->query->has('page')) {
+      $options['page'] = new OffsetPage($request->query->get('page'), OffsetPage::$maxSize);
+    }
+    else {
+      $options['page'] = new OffsetPage(['start' => 0, 'limit' => OffsetPage::$maxSize], OffsetPage::$maxSize);
+    }
+    $defaults['_json_api_params'] = $options;
+    return $defaults;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Routing/Param/Filter.php b/drupal/modules/jsonapi/src/Routing/Param/Filter.php
new file mode 100644
index 0000000..1fd14dd
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Routing/Param/Filter.php
@@ -0,0 +1,310 @@
+<?php
+
+namespace Drupal\jsonapi\Routing\Param;
+
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+use Drupal\Component\Render\FormattableMarkup;
+
+/**
+ * @internal
+ */
+class Filter extends JsonApiParamBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  const KEY_NAME = 'filter';
+
+  /**
+   * Key in the filter[<key>] parameter for conditions.
+   *
+   * @var string
+   */
+  const CONDITION_KEY = 'condition';
+
+  /**
+   * Key in the filter[<key>] parameter for groups.
+   *
+   * @var string
+   */
+  const GROUP_KEY = 'group';
+
+  /**
+   * Key in the filter[<id>][<key>] parameter for group membership.
+   *
+   * @var string
+   */
+  const MEMBER_KEY = 'memberOf';
+
+  /**
+   * The field key in the filter condition: filter[lorem][condition][<field>].
+   *
+   * @var string
+   */
+  const PATH_KEY = 'path';
+
+  /**
+   * The value key in the filter condition: filter[lorem][condition][<value>].
+   *
+   * @var string
+   */
+  const VALUE_KEY = 'value';
+
+  /**
+   * The operator key in the condition: filter[lorem][condition][<operator>].
+   *
+   * @var string
+   */
+  const OPERATOR_KEY = 'operator';
+
+  /**
+   * The conjunction key in the condition: filter[lorem][group][<conjunction>].
+   *
+   * @var string
+   */
+  const CONJUNCTION_KEY = 'conjunction';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function expand() {
+    // We should always get an array for the filter.
+    if (!is_array($this->original)) {
+      throw new BadRequestHttpException('Incorrect value passed to the filter parameter.');
+    }
+
+    $expanded = [];
+    foreach ($this->original as $filter_index => $filter_item) {
+      $expanded_item = $this->expandItem($filter_index, $filter_item);
+      $this->validateItem($filter_index, $expanded_item);
+      $expanded[$filter_index] = $expanded_item;
+    }
+    return $expanded;
+  }
+
+  /**
+   * Expands a filter item in case a shortcut was used.
+   *
+   * Possible cases for the conditions:
+   *   1. filter[uuid][value]=1234.
+   *   2. filter[0][condition][path]=uuid&filter[0][condition][value]=1234.
+   *   3. filter[uuid][value]=1234&filter[uuid][path]=uuid&
+   *      filter[uuid][operator]==&filter[uuid][memberOf]=my_group.
+   *
+   * @param string $filter_index
+   *   The index.
+   * @param mixed $filter_item
+   *   The raw filter item.
+   *
+   * @return array
+   *   The expanded filter item.
+   */
+  protected function expandItem($filter_index, $filter_item) {
+    // Expand shorthand.
+    if (isset($filter_item[static::VALUE_KEY])) {
+      if (!isset($filter_item[static::PATH_KEY])) {
+        $filter_item[static::PATH_KEY] = $filter_index;
+      }
+      $filter_item = [
+        static::CONDITION_KEY => $filter_item,
+      ];
+
+      if (!isset($filter_item[static::CONDITION_KEY][static::OPERATOR_KEY])) {
+        $filter_item[static::CONDITION_KEY][static::OPERATOR_KEY] = '=';
+      }
+    }
+    // Expand IS (NOT) NULL items.
+    elseif (
+      isset($filter_item[static::CONDITION_KEY][static::OPERATOR_KEY]) &&
+      in_array($filter_item[static::CONDITION_KEY][static::OPERATOR_KEY], ['IS NULL', 'IS NOT NULL'])
+    ) {
+      // This is not strictly necessary, but it simplifies validation.
+      $filter_item[static::CONDITION_KEY][static::VALUE_KEY] = NULL;
+    }
+
+    return $filter_item;
+  }
+
+  /**
+   * Makes sure every filter item contains valid data.
+   *
+   * @param string $filter_index
+   *   The index.
+   * @param mixed $filter_item
+   *   The expanded filter item.
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
+   *   If the filter is malformed.
+   */
+  protected function validateItem($filter_index, $filter_item) {
+    // Make sure the current filter item is an array. So far we don't allow
+    // filter queries like filter[nid]=1.
+    if (!is_array($filter_item)) {
+      $message = new FormattableMarkup(
+        'Filter query for "@index" must be array.',
+        ['@index' => $filter_index]
+      );
+      throw new BadRequestHttpException($message);
+    }
+
+    // We do not allow having both "condition" and "group" for the same filter
+    // item. So for example this condition should fail:
+    // - filter[id][condition][path]=nid
+    // - filter[id][condition][value]=123
+    // - filter[id][condition][operator]==
+    // - filter[id][group][conjunction]=AND.
+    $filter_keys = array_keys($filter_item);
+    $allowed_filter_keys = [static::CONDITION_KEY, static::GROUP_KEY];
+    if (count($filter_keys) > 1 || !in_array($filter_keys[0], $allowed_filter_keys)) {
+      $message = new FormattableMarkup(
+        'Filter query for "@index" should only contain either "@condkey" or "@groupkey" as a top-level key, but not both.',
+        [
+          '@index' => $filter_index,
+          '@condkey' => static::CONDITION_KEY,
+          '@groupkey' => static::GROUP_KEY,
+        ]
+      );
+      throw new BadRequestHttpException($message);
+    }
+
+    // Handle full canonical form:
+    // - filter[id][condition][path]=nid
+    // - filter[id][condition][value]=123
+    // - filter[id][condition][operator]==
+    // - filter[id][condition][memberOf]=some-group.
+    if (isset($filter_item[static::CONDITION_KEY])) {
+      $this->validateConditionItem($filter_index, $filter_item);
+    }
+    // Validating filter groups. Example of group:
+    // filter[and-group][group][conjunction]=AND
+    // filter[and-group][group][memberOf]=another-group.
+    elseif (isset($filter_item[static::GROUP_KEY])) {
+      $this->validateGroupItem($filter_index, $filter_item);
+    }
+
+  }
+
+  /**
+   * Makes sure a condition in a filter item contains valid data.
+   *
+   * @param string $filter_index
+   *   The index.
+   * @param mixed $filter_item
+   *   The expanded filter item.
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
+   *   If the filter is malformed.
+   */
+  protected function validateConditionItem($filter_index, $filter_item) {
+    // List of allowed keys for adding a new condition to the query.
+    $expected_keys = [
+      static::PATH_KEY,
+      static::VALUE_KEY,
+      static::OPERATOR_KEY,
+      static::MEMBER_KEY,
+    ];
+
+    // Get keys sent by the client.
+    $item_keys = array_keys($filter_item[static::CONDITION_KEY]);
+
+    // If the client sent any keys outside of allowed, we should return an
+    // error to indicate that the desired set of params is not going to work.
+    $unexpected_keys = array_diff($item_keys, $expected_keys);
+    if (!empty($unexpected_keys)) {
+      $message = new FormattableMarkup(
+        'Filter query for "@index" contains not expected arguments: @invalid_args. Valid arguments: @valid_args',
+        [
+          '@index' => $filter_index,
+          '@invalid_args' => implode(',', $unexpected_keys),
+          '@valid_args' => implode(',', $expected_keys),
+        ]
+      );
+      throw new BadRequestHttpException($message);
+    }
+
+    // It is required to set the next keys for full canonical filter
+    // representation.
+    $mandatory_keys = [static::PATH_KEY, static::VALUE_KEY];
+
+    // If any mandatory key is missing - report back to the client.
+    $missing_mandatory_keys = array_diff($mandatory_keys, $item_keys);
+    if (!empty($missing_mandatory_keys)) {
+      $message = new FormattableMarkup(
+        'Filter query for "@index" missing mandatory params: @missing_params.',
+        [
+          '@index' => $filter_index,
+          '@missing_params' => implode(',', $missing_mandatory_keys),
+        ]
+      );
+      throw new BadRequestHttpException($message);
+    }
+  }
+
+  /**
+   * Makes sure a condition in a filter item contains valid data.
+   *
+   * @param string $filter_index
+   *   The index.
+   * @param mixed $filter_item
+   *   The expanded filter item.
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
+   *   If the filter is malformed.
+   */
+  protected function validateGroupItem($filter_index, $filter_item) {
+    // List of allowed keys for adding a new filter group.
+    $expected_keys = [static::CONJUNCTION_KEY, static::MEMBER_KEY];
+
+    // If the client sent any keys outside of allowed, we should return an
+    // error to indicate that the desired set of params is not going to work.
+    $item_keys = array_keys($filter_item[static::GROUP_KEY]);
+    $unexpected_keys = array_diff($item_keys, $expected_keys);
+    if (!empty($unexpected_keys)) {
+      $message = new FormattableMarkup(
+        'Filter query for "@index" contains unexpected arguments: @invalid_args. Valid arguments: @valid_args',
+        [
+          '@index' => $filter_index,
+          '@invalid_args' => implode(',', $unexpected_keys),
+          '@valid_args' => implode(',', $expected_keys),
+        ]
+      );
+      throw new BadRequestHttpException($message);
+    }
+
+    // If any mandatory key is missing - report back to the client.
+    $mandatory_keys = [static::CONJUNCTION_KEY];
+    $missing_mandatory_keys = array_diff($mandatory_keys, $item_keys);
+    if (!empty($missing_mandatory_keys)) {
+      $message = new FormattableMarkup(
+        'Filter query for "@index" missing mandatory params: @missing_params.',
+        [
+          '@index' => $filter_index,
+          '@missing_params' => implode(',', $missing_mandatory_keys),
+        ]
+      );
+      throw new BadRequestHttpException($message);
+    }
+
+    // Make sure the conjunction value is correct.
+    $allowed_conjunction = ['AND', 'OR', 'and', 'or'];
+    if (!in_array($filter_item[static::GROUP_KEY][static::CONJUNCTION_KEY], $allowed_conjunction)) {
+      $message = new FormattableMarkup(
+        'Filter query for "@index" contains invalid conjunction operator. Allowed values: AND, OR.',
+        ['@index' => $filter_index]
+      );
+      throw new BadRequestHttpException($message);
+    }
+
+    // We do not allow having anything other than "group" in the filter query.
+    // So for example this condition should fail:
+    // - filter[][group][conjunction]=AND
+    // - filter[][nid][value]=123.
+    if (count($filter_item) > 1) {
+      $message = new FormattableMarkup(
+        'Filter query for "@index" should contain only "@key" as a top-level key.',
+        ['@index' => $filter_index, '@key' => static::GROUP_KEY]);
+      throw new BadRequestHttpException($message);
+    }
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Routing/Param/JsonApiParamBase.php b/drupal/modules/jsonapi/src/Routing/Param/JsonApiParamBase.php
new file mode 100644
index 0000000..21cb503
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Routing/Param/JsonApiParamBase.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace Drupal\jsonapi\Routing\Param;
+
+/**
+ * @internal
+ */
+class JsonApiParamBase implements JsonApiParamInterface {
+
+  /**
+   * The original data.
+   *
+   * @var string|string[]
+   */
+  protected $original;
+
+  /**
+   * The expanded data.
+   *
+   * @var string|string[]
+   */
+  protected $data;
+
+  /**
+   * Create a parameter object.
+   *
+   * @param string|string[] $original
+   *   The user generated data.
+   */
+  public function __construct($original) {
+    $this->original = $original;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function get() {
+    if (!$this->data) {
+      $this->data = $this->expand();
+    }
+    return $this->data;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOriginal() {
+    return $this->original;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getKey() {
+    return static::KEY_NAME;
+  }
+
+  /**
+   * Apply all necessary defaults and transformations to the parameter.
+   *
+   * @return string|string[]
+   *   The expanded data.
+   */
+  protected function expand() {
+    // The base implementation does no expansion.
+    return $this->original;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Routing/Param/JsonApiParamInterface.php b/drupal/modules/jsonapi/src/Routing/Param/JsonApiParamInterface.php
new file mode 100644
index 0000000..695aa37
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Routing/Param/JsonApiParamInterface.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Drupal\jsonapi\Routing\Param;
+
+/**
+ * @internal
+ */
+interface JsonApiParamInterface {
+
+  /**
+   * The key name.
+   *
+   * This must be redefined with a unique value in each class that extends
+   * from JsonApiParamInterface.
+   *
+   * @var string
+   */
+  const KEY_NAME = NULL;
+
+  /**
+   * Gets the original parsed query string param.
+   *
+   * @return string|string[]
+   *   The original value.
+   */
+  public function getOriginal();
+
+  /**
+   * Gets the expanded value with defaults.
+   *
+   * @return string|string[]
+   *   The query string value.
+   */
+  public function get();
+
+  /**
+   * Gets the key of the parameter.
+   *
+   * @return string
+   *   The key.
+   */
+  public function getKey();
+
+}
diff --git a/drupal/modules/jsonapi/src/Routing/Param/OffsetPage.php b/drupal/modules/jsonapi/src/Routing/Param/OffsetPage.php
new file mode 100644
index 0000000..2de1b22
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Routing/Param/OffsetPage.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace Drupal\jsonapi\Routing\Param;
+
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+
+/**
+ * @internal
+ */
+class OffsetPage extends JsonApiParamBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  const KEY_NAME = 'page';
+
+  /**
+   * Max size.
+   *
+   * @var int
+   */
+  public static $maxSize = 50;
+
+  /**
+   * Instantiates an OffsetPage object.
+   *
+   * @param string|\string[] $original
+   *   The original user generated data.
+   * @param int $max_size
+   *   The maximum size for the pager.
+   */
+  public function __construct($original, $max_size = NULL) {
+    parent::__construct($original);
+    if ($max_size) {
+      static::$maxSize = $max_size;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function expand() {
+    if (!is_array($this->original)) {
+      throw new BadRequestHttpException('The page parameter needs to be an array.');
+    }
+    $output = $this->original + ['limit' => static::$maxSize];
+    $output['limit'] = $output['limit'] > static::$maxSize ?
+      static::$maxSize :
+      $output['limit'];
+    return $output;
+  }
+
+  /**
+   * Returns the current offset.
+   *
+   * @return int
+   */
+  public function getOffset() {
+    $data = $this->get();
+    return isset($data['offset']) ? $data['offset'] : 0;
+  }
+
+  /**
+   * Returns the page size.
+   *
+   * @return int
+   */
+  public function getSize() {
+    $data = $this->get();
+    $size = isset($data['limit']) ? $data['limit'] : static::$maxSize;
+    return $size > static::$maxSize ? static::$maxSize : $size;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Routing/Param/Sort.php b/drupal/modules/jsonapi/src/Routing/Param/Sort.php
new file mode 100644
index 0000000..eee57d9
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Routing/Param/Sort.php
@@ -0,0 +1,131 @@
+<?php
+
+namespace Drupal\jsonapi\Routing\Param;
+
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+
+/**
+ * @internal
+ */
+class Sort extends JsonApiParamBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  const KEY_NAME = 'sort';
+
+  /**
+   * The field key in the sort parameter: sort[lorem][<field>].
+   *
+   * @var string
+   */
+  const FIELD_KEY = 'path';
+
+  /**
+   * The direction key in the sort parameter: sort[lorem][<direction>].
+   *
+   * @var string
+   */
+  const DIRECTION_KEY = 'direction';
+
+  /**
+   * The langcode key in the sort parameter: sort[lorem][<langcode>].
+   *
+   * @var string
+   */
+  const LANGUAGE_KEY = 'langcode';
+
+  /**
+   * The conjunction key in the condition: filter[lorem][group][<conjunction>].
+   *
+   * @var string
+   */
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function expand() {
+    $sort = $this->original;
+
+    if (empty($sort)) {
+      throw new BadRequestHttpException('You need to provide a value for the sort parameter.');
+    }
+
+    // Expand a JSON API compliant sort into a more expressive sort parameter.
+    if (is_string($sort)) {
+      $sort = $this->expandFieldString($sort);
+    }
+
+    // Expand any defaults into the sort array.
+    $expanded = [];
+    foreach ($sort as $sort_index => $sort_item) {
+      $expanded[$sort_index] = $this->expandItem($sort_index, $sort_item);
+    }
+
+    return $expanded;
+  }
+
+  /**
+   * Expands a simple string sort into a more expressive sort that we can use.
+   *
+   * @param string $fields
+   *   The comma separated list of fields to expand into an array.
+   *
+   * @return array
+   *   The expanded sort.
+   */
+  protected function expandFieldString($fields) {
+    return array_map(function ($field) {
+      $sort = [];
+
+      if ($field[0] == '-') {
+        $sort[static::DIRECTION_KEY] = 'DESC';
+        $sort[static::FIELD_KEY] = substr($field, 1);
+      }
+      else {
+        $sort[static::DIRECTION_KEY] = 'ASC';
+        $sort[static::FIELD_KEY] = $field;
+      }
+
+      return $sort;
+    }, explode(',', $fields));
+  }
+
+  /**
+   * Expands a sort item in case a shortcut was used.
+   *
+   * @param string $sort_index
+   *   Unique identifier for the sort parameter being expanded.
+   * @param array $sort_item
+   *   The raw sort item.
+   *
+   * @return array
+   *   The expanded sort item.
+   */
+  protected function expandItem($sort_index, array $sort_item) {
+    $defaults = [
+      static::DIRECTION_KEY => 'ASC',
+      static::LANGUAGE_KEY => NULL,
+    ];
+
+    if (!isset($sort_item[static::FIELD_KEY])) {
+      throw new BadRequestHttpException('You need to provide a field name for the sort parameter.');
+    }
+
+    $expected_keys = [
+      static::FIELD_KEY,
+      static::DIRECTION_KEY,
+      static::LANGUAGE_KEY,
+    ];
+
+    $expanded = array_merge($defaults, $sort_item);
+
+    // Verify correct sort keys.
+    if (count(array_diff($expected_keys, array_keys($expanded))) > 0) {
+      throw new BadRequestHttpException('You have provided an invalid set of sort keys.');
+    }
+
+    return $expanded;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Routing/RouteEnhancer.php b/drupal/modules/jsonapi/src/Routing/RouteEnhancer.php
new file mode 100644
index 0000000..2539602
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Routing/RouteEnhancer.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Drupal\jsonapi\Routing;
+
+use Drupal\Core\Routing\Enhancer\RouteEnhancerInterface;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+use Symfony\Component\Routing\Route;
+
+/**
+ * @internal
+ */
+class RouteEnhancer implements RouteEnhancerInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applies(Route $route) {
+    return (bool) $route->getRequirement('_bundle') && (bool) $route->getRequirement('_entity_type');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function enhance(array $defaults, Request $request) {
+    $route = $defaults[RouteObjectInterface::ROUTE_OBJECT];
+    $entity_type = $route->getRequirement('_entity_type');
+    if (!isset($defaults[$entity_type]) || !($entity = $defaults[$entity_type])) {
+      return $defaults;
+    }
+    $retrieved_bundle = $entity->bundle();
+    $configured_bundle = $route->getRequirement('_bundle');
+    if ($retrieved_bundle != $configured_bundle) {
+      // If the bundle in the loaded entity does not match the bundle in the
+      // route (which is set based on the corresponding ResourceType), then
+      // throw an exception.
+      throw new NotFoundHttpException(sprintf('The loaded entity bundle (%s) does not match the configured resource (%s).', $retrieved_bundle, $configured_bundle));
+    }
+    return $defaults;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/Routing/Routes.php b/drupal/modules/jsonapi/src/Routing/Routes.php
new file mode 100644
index 0000000..00b49b7
--- /dev/null
+++ b/drupal/modules/jsonapi/src/Routing/Routes.php
@@ -0,0 +1,187 @@
+<?php
+
+namespace Drupal\jsonapi\Routing;
+
+use Drupal\Core\Authentication\AuthenticationCollectorInterface;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Field\EntityReferenceFieldItemList;
+use Drupal\jsonapi\ResourceType\ResourceTypeRepository;
+use Drupal\jsonapi\Resource\JsonApiDocumentTopLevel;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * Defines dynamic routes.
+ *
+ * @internal
+ */
+class Routes implements ContainerInjectionInterface {
+
+  /**
+   * The front controller for the JSON API routes.
+   *
+   * All routes will use this callback to bootstrap the JSON API process.
+   *
+   * @var string
+   */
+  const FRONT_CONTROLLER = '\Drupal\jsonapi\Controller\RequestHandler::handle';
+
+  /**
+   * The JSON API resource type repository.
+   *
+   * @var \Drupal\jsonapi\ResourceType\ResourceTypeRepository
+   */
+  protected $resourceTypeRepository;
+
+  /**
+   * The authentication collector.
+   *
+   * @var \Drupal\Core\Authentication\AuthenticationCollectorInterface
+   */
+  protected $authCollector;
+
+  /**
+   * List of providers.
+   *
+   * @var string[]
+   */
+  protected $providerIds;
+
+  /**
+   * Instantiates a Routes object.
+   *
+   * @param \Drupal\jsonapi\ResourceType\ResourceTypeRepository $resource_type_repository
+   *   The JSON API resource type repository.
+   * @param \Drupal\Core\Authentication\AuthenticationCollectorInterface $auth_collector
+   *   The authentication provider collector.
+   */
+  public function __construct(ResourceTypeRepository $resource_type_repository, AuthenticationCollectorInterface $auth_collector) {
+    $this->resourceTypeRepository = $resource_type_repository;
+    $this->authCollector = $auth_collector;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    /* @var \Drupal\jsonapi\ResourceType\ResourceTypeRepository $resource_type_repository */
+    $resource_type_repository = $container->get('jsonapi.resource_type.repository');
+    /* @var \Drupal\Core\Authentication\AuthenticationCollectorInterface $auth_collector */
+    $auth_collector = $container->get('authentication_collector');
+
+    return new static($resource_type_repository, $auth_collector);
+  }
+
+  /**
+   * Provides the entry point route.
+   */
+  public function entryPoint() {
+    $collection = new RouteCollection();
+
+    $route_collection = (new Route('/jsonapi', [
+      RouteObjectInterface::CONTROLLER_NAME => '\Drupal\jsonapi\Controller\EntryPoint::index',
+    ]))
+      ->setRequirement('_permission', 'access jsonapi resource list')
+      ->setMethods(['GET']);
+    $route_collection->addOptions([
+      '_auth' => $this->authProviderList(),
+      '_is_jsonapi' => TRUE,
+    ]);
+    $collection->add('jsonapi.resource_list', $route_collection);
+
+    return $collection;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function routes() {
+    $collection = new RouteCollection();
+    foreach ($this->resourceTypeRepository->all() as $resource_type) {
+      $route_base_path = sprintf('/jsonapi/%s/%s', $resource_type->getEntityTypeId(), $resource_type->getBundle());
+      $build_route_name = function ($key) use ($resource_type) {
+        return sprintf('jsonapi.%s.%s', $resource_type->getTypeName(), $key);
+      };
+
+      $defaults = [
+        RouteObjectInterface::CONTROLLER_NAME => static::FRONT_CONTROLLER,
+      ];
+      // Options that apply to all routes.
+      $options = [
+        '_auth' => $this->authProviderList(),
+        '_is_jsonapi' => TRUE,
+      ];
+
+      // Collection endpoint, like /jsonapi/file/photo.
+      $route_collection = (new Route($route_base_path, $defaults))
+        ->setRequirement('_entity_type', $resource_type->getEntityTypeId())
+        ->setRequirement('_bundle', $resource_type->getBundle())
+        ->setRequirement('_permission', 'access content')
+        ->setRequirement('_jsonapi_custom_query_parameter_names', 'TRUE')
+        ->setOption('serialization_class', JsonApiDocumentTopLevel::class)
+        ->setMethods(['GET', 'POST']);
+      $route_collection->addOptions($options);
+      $collection->add($build_route_name('collection'), $route_collection);
+
+      // Individual endpoint, like /jsonapi/file/photo/123.
+      $parameters = [$resource_type->getEntityTypeId() => ['type' => 'entity:' . $resource_type->getEntityTypeId()]];
+      $route_individual = (new Route(sprintf('%s/{%s}', $route_base_path, $resource_type->getEntityTypeId())))
+        ->addDefaults($defaults)
+        ->setRequirement('_entity_type', $resource_type->getEntityTypeId())
+        ->setRequirement('_bundle', $resource_type->getBundle())
+        ->setRequirement('_permission', 'access content')
+        ->setRequirement('_jsonapi_custom_query_parameter_names', 'TRUE')
+        ->setOption('parameters', $parameters)
+        ->setOption('_auth', $this->authProviderList())
+        ->setOption('serialization_class', JsonApiDocumentTopLevel::class)
+        ->setMethods(['GET', 'PATCH', 'DELETE']);
+      $route_individual->addOptions($options);
+      $collection->add($build_route_name('individual'), $route_individual);
+
+      // Related resource, like /jsonapi/file/photo/123/comments.
+      $route_related = (new Route(sprintf('%s/{%s}/{related}', $route_base_path, $resource_type->getEntityTypeId()), $defaults))
+        ->setRequirement('_entity_type', $resource_type->getEntityTypeId())
+        ->setRequirement('_bundle', $resource_type->getBundle())
+        ->setRequirement('_permission', 'access content')
+        ->setRequirement('_jsonapi_custom_query_parameter_names', 'TRUE')
+        ->setOption('parameters', $parameters)
+        ->setOption('_auth', $this->authProviderList())
+        ->setMethods(['GET']);
+      $route_related->addOptions($options);
+      $collection->add($build_route_name('related'), $route_related);
+
+      // Related endpoint, like /jsonapi/file/photo/123/relationships/comments.
+      $route_relationship = (new Route(sprintf('%s/{%s}/relationships/{related}', $route_base_path, $resource_type->getEntityTypeId()), $defaults + ['_on_relationship' => TRUE]))
+        ->setRequirement('_entity_type', $resource_type->getEntityTypeId())
+        ->setRequirement('_bundle', $resource_type->getBundle())
+        ->setRequirement('_permission', 'access content')
+        ->setRequirement('_jsonapi_custom_query_parameter_names', 'TRUE')
+        ->setOption('parameters', $parameters)
+        ->setOption('_auth', $this->authProviderList())
+        ->setOption('serialization_class', EntityReferenceFieldItemList::class)
+        ->setMethods(['GET', 'POST', 'PATCH', 'DELETE']);
+      $route_relationship->addOptions($options);
+      $collection->add($build_route_name('relationship'), $route_relationship);
+    }
+
+    return $collection;
+  }
+
+  /**
+   * Build a list of authentication provider ids.
+   *
+   * @return string[]
+   *   The list of IDs.
+   */
+  protected function authProviderList() {
+    if (isset($this->providerIds)) {
+      return $this->providerIds;
+    }
+    $this->providerIds = array_keys($this->authCollector->getSortedProviders());
+
+    return $this->providerIds;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/src/StackMiddleware/FormatSetter.php b/drupal/modules/jsonapi/src/StackMiddleware/FormatSetter.php
new file mode 100644
index 0000000..05c3335
--- /dev/null
+++ b/drupal/modules/jsonapi/src/StackMiddleware/FormatSetter.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Drupal\jsonapi\StackMiddleware;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
+
+/**
+ * Sets the 'api_json' format on all requests to JSON API-managed routes.
+ */
+class FormatSetter implements HttpKernelInterface {
+
+  /**
+   * The wrapped HTTP kernel.
+   *
+   * @var \Symfony\Component\HttpKernel\HttpKernelInterface
+   */
+  protected $httpKernel;
+
+  /**
+   * Constructs a FormatSetter object.
+   *
+   * @param \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel
+   *   The decorated kernel.
+   */
+  public function __construct(HttpKernelInterface $http_kernel) {
+    $this->httpKernel = $http_kernel;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) {
+    if (static::isJsonApiRequest($request)) {
+      $request->setRequestFormat('api_json');
+    }
+
+    return $this->httpKernel->handle($request, $type, $catch);
+  }
+
+  /**
+   * Checks whether the current request is a JSON API request.
+   *
+   * Inspects:
+   * - request path (uses a heuristic, because e.g. language negotiation may use
+   *   path prefixes)
+   * - 'Accept' request header value.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request.
+   *
+   * @return bool
+   *   Whether the current request is a JSON API request.
+   */
+  protected static function isJsonApiRequest(Request $request) {
+    return strpos($request->getPathInfo(), '/jsonapi/') !== FALSE
+      &&
+      // Check if the 'Accept' header includes the JSON API MIME type.
+      count(array_filter($request->getAcceptableContentTypes(), function ($accept) {
+        return strpos($accept, 'application/vnd.api+json') === 0;
+      }));
+  }
+
+}
diff --git a/drupal/modules/jsonapi/tests/phpunit.xml b/drupal/modules/jsonapi/tests/phpunit.xml
new file mode 100644
index 0000000..b3bff7a
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/phpunit.xml
@@ -0,0 +1,18 @@
+<!--?xml version="1.0" encoding="UTF-8"?-->
+
+<phpunit colors="true">
+  <testsuites>
+    <testsuite name="jsonapi">
+      <directory>./src/</directory>
+    </testsuite>
+  </testsuites>
+  <!-- Filter for coverage reports. -->
+  <filter>
+    <blacklist>
+      <directory>./vendor</directory>
+    </blacklist>
+    <whitelist>
+      <directory>../src</directory>
+    </whitelist>
+  </filter>
+</phpunit>
diff --git a/drupal/modules/jsonapi/tests/src/Functional/JsonApiFunctionalMultilingualTest.php b/drupal/modules/jsonapi/tests/src/Functional/JsonApiFunctionalMultilingualTest.php
new file mode 100644
index 0000000..998f414
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Functional/JsonApiFunctionalMultilingualTest.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Functional;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\language\Entity\ConfigurableLanguage;
+
+/**
+ * @group jsonapi
+ */
+class JsonApiFunctionalMultilingualTest extends JsonApiFunctionalTestBase {
+
+  public static $modules = [
+    'basic_auth',
+    'jsonapi',
+    'serialization',
+    'node',
+    'image',
+    'taxonomy',
+    'link',
+    'language',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $language = ConfigurableLanguage::createFromLangcode('ca');
+    $language->save();
+
+    // In order to reflect the changes for a multilingual site in the container
+    // we have to rebuild it.
+    $this->rebuildContainer();
+
+    \Drupal::configFactory()->getEditable('language.negotiation')
+      ->set('url.prefixes.ca', 'ca')
+      ->save();
+  }
+
+  /**
+   * Tests reading multilingual content.
+   */
+  public function testReadMultilingual() {
+    $this->createDefaultContent(5, 5, TRUE, TRUE, static::IS_MULTILINGUAL);
+
+    // Test reading an individual entity.
+    $output = Json::decode($this->drupalGet('/ca/jsonapi/node/article/' . $this->nodes[0]->uuid(), ['query' => ['include' => 'field_tags,field_image']]));
+    $this->assertEquals($this->nodes[0]->getTranslation('ca')->getTitle(), $output['data']['attributes']['title']);
+
+    $included_tags = array_filter($output['included'], function ($entry) {
+      return $entry['type'] === 'taxonomy_term--tags';
+    });
+    $tag_name = $this->nodes[0]->get('field_tags')->entity
+      ->getTranslation('ca')->getName();
+    // TODO figure out how to fetcht the alt text of an image.
+    $this->assertEquals($tag_name, reset($included_tags)['attributes']['name']);
+
+    $output = Json::decode($this->drupalGet('/ca/jsonapi/node/article/' . $this->nodes[0]->uuid()));
+    $this->assertEquals($this->nodes[0]->getTranslation('ca')->getTitle(), $output['data']['attributes']['title']);
+
+    // Test reading a collection of entities.
+    $output = Json::decode($this->drupalGet('/ca/jsonapi/node/article'));
+    $this->assertEquals($this->nodes[0]->getTranslation('ca')->getTitle(), $output['data'][0]['attributes']['title']);
+  }
+
+}
diff --git a/drupal/modules/jsonapi/tests/src/Functional/JsonApiFunctionalTest.php b/drupal/modules/jsonapi/tests/src/Functional/JsonApiFunctionalTest.php
new file mode 100644
index 0000000..4236086
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Functional/JsonApiFunctionalTest.php
@@ -0,0 +1,595 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Functional;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Core\Url;
+use Drupal\jsonapi\Routing\Param\OffsetPage;
+
+/**
+ * @group jsonapi
+ */
+class JsonApiFunctionalTest extends JsonApiFunctionalTestBase {
+
+  /**
+   * Test the GET method.
+   */
+  public function testRead() {
+    $this->createDefaultContent(61, 5, TRUE, TRUE, static::IS_NOT_MULTILINGUAL);
+    // Unpublish the last entity, so we can check access.
+    $this->nodes[60]->setUnpublished()->save();
+
+    // 1. Load all articles (1st page).
+    $collection_output = Json::decode($this->drupalGet('/jsonapi/node/article'));
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertEquals(OffsetPage::$maxSize, count($collection_output['data']));
+    $this->assertSession()
+      ->responseHeaderEquals('Content-Type', 'application/vnd.api+json');
+    // 2. Load all articles (Offset 3).
+    $collection_output = Json::decode($this->drupalGet('/jsonapi/node/article', [
+      'query' => ['page' => ['offset' => 3]],
+    ]));
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertEquals(OffsetPage::$maxSize, count($collection_output['data']));
+    $this->assertContains('page%5Boffset%5D=53', $collection_output['links']['next']);
+    // 3. Load all articles (1st page, 2 items)
+    $collection_output = Json::decode($this->drupalGet('/jsonapi/node/article', [
+      'query' => ['page' => ['limit' => 2]],
+    ]));
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertEquals(2, count($collection_output['data']));
+    // 4. Load all articles (2nd page, 2 items).
+    $collection_output = Json::decode($this->drupalGet('/jsonapi/node/article', [
+      'query' => [
+        'page' => [
+          'limit' => 2,
+          'offset' => 2,
+        ],
+      ],
+    ]));
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertEquals(2, count($collection_output['data']));
+    $this->assertContains('page%5Boffset%5D=4', $collection_output['links']['next']);
+    // 5. Single article.
+    $uuid = $this->nodes[0]->uuid();
+    $single_output = Json::decode($this->drupalGet('/jsonapi/node/article/' . $uuid));
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertArrayHasKey('type', $single_output['data']);
+    $this->assertEquals($this->nodes[0]->getTitle(), $single_output['data']['attributes']['title']);
+
+    // 5.1 Single article with access denied.
+    $single_output = Json::decode($this->drupalGet('/jsonapi/node/article/' . $this->nodes[60]->uuid()));
+    $this->assertSession()->statusCodeEquals(403);
+
+    $this->assertEquals('/data', $single_output['errors'][0]['source']['pointer']);
+    $this->assertEquals('/node--article/' . $this->nodes[60]->uuid(), $single_output['errors'][0]['id']);
+
+    // 6. Single relationship item.
+    $single_output = Json::decode($this->drupalGet('/jsonapi/node/article/' . $uuid . '/relationships/type'));
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertArrayHasKey('type', $single_output['data']);
+    $this->assertArrayNotHasKey('attributes', $single_output['data']);
+    $this->assertArrayHasKey('related', $single_output['links']);
+    // 7. Single relationship image.
+    $single_output = Json::decode($this->drupalGet('/jsonapi/node/article/' . $uuid . '/relationships/field_image'));
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertArrayHasKey('type', $single_output['data']);
+    $this->assertArrayNotHasKey('attributes', $single_output['data']);
+    $this->assertArrayHasKey('related', $single_output['links']);
+    // 8. Multiple relationship item.
+    $single_output = Json::decode($this->drupalGet('/jsonapi/node/article/' . $uuid . '/relationships/field_tags'));
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertArrayHasKey('type', $single_output['data'][0]);
+    $this->assertArrayNotHasKey('attributes', $single_output['data'][0]);
+    $this->assertArrayHasKey('related', $single_output['links']);
+    // 9. Related tags with includes.
+    $single_output = Json::decode($this->drupalGet('/jsonapi/node/article/' . $uuid . '/field_tags', [
+      'query' => ['include' => 'vid'],
+    ]));
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertEquals('taxonomy_term--tags', $single_output['data'][0]['type']);
+    $this->assertArrayHasKey('tid', $single_output['data'][0]['attributes']);
+    $this->assertContains(
+      '/taxonomy_term/tags/',
+      $single_output['data'][0]['links']['self']
+    );
+    $this->assertEquals(
+      'taxonomy_vocabulary--taxonomy_vocabulary',
+      $single_output['included'][0]['type']
+    );
+    // 10. Single article with includes.
+    $single_output = Json::decode($this->drupalGet('/jsonapi/node/article/' . $uuid, [
+      'query' => ['include' => 'uid,field_tags'],
+    ]));
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertEquals('node--article', $single_output['data']['type']);
+    $first_include = reset($single_output['included']);
+    $this->assertEquals(
+      'user--user',
+      $first_include['type']
+    );
+    $last_include = end($single_output['included']);
+    $this->assertEquals(
+      'taxonomy_term--tags',
+      $last_include['type']
+    );
+    // 11. Includes with relationships.
+    $single_output = Json::decode($this->drupalGet('/jsonapi/node/article/' . $uuid . '/relationships/uid', [
+      'query' => ['include' => 'uid'],
+    ]));
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertEquals('user--user', $single_output['data']['type']);
+    $this->assertArrayHasKey('related', $single_output['links']);
+    $first_include = reset($single_output['included']);
+    $this->assertEquals(
+      'user--user',
+      $first_include['type']
+    );
+    $this->assertFalse(empty($first_include['attributes']));
+    $this->assertTrue(empty($first_include['attributes']['mail']));
+    $this->assertTrue(empty($first_include['attributes']['pass']));
+    // 12. Collection with one access denied.
+    $this->nodes[1]->set('status', FALSE);
+    $this->nodes[1]->save();
+    $single_output = Json::decode($this->drupalGet('/jsonapi/node/article', [
+      'query' => ['page' => ['limit' => 2]],
+    ]));
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertEquals(1, count($single_output['data']));
+    $this->assertEquals(1, count($single_output['meta']['errors']));
+    $this->assertEquals(403, $single_output['meta']['errors'][0]['status']);
+    $this->assertEquals('/node--article/' . $this->nodes[1]->uuid(), $single_output['meta']['errors'][0]['id']);
+    $this->assertFalse(empty($single_output['meta']['errors'][0]['source']['pointer']));
+    $this->nodes[1]->set('status', TRUE);
+    $this->nodes[1]->save();
+    // 13. Test filtering when using short syntax.
+    $filter = [
+      'uid.uuid' => ['value' => $this->user->uuid()],
+      'field_tags.uuid' => ['value' => $this->tags[0]->uuid()],
+    ];
+    $single_output = Json::decode($this->drupalGet('/jsonapi/node/article', [
+      'query' => ['filter' => $filter, 'include' => 'uid,field_tags'],
+    ]));
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertGreaterThan(0, count($single_output['data']));
+    // 14. Test filtering when using long syntax.
+    $filter = [
+      'and_group' => ['group' => ['conjunction' => 'AND']],
+      'filter_user' => [
+        'condition' => [
+          'path' => 'uid.uuid',
+          'value' => $this->user->uuid(),
+          'memberOf' => 'and_group',
+        ],
+      ],
+      'filter_tags' => [
+        'condition' => [
+          'path' => 'field_tags.uuid',
+          'value' => $this->tags[0]->uuid(),
+          'memberOf' => 'and_group',
+        ],
+      ],
+    ];
+    $single_output = Json::decode($this->drupalGet('/jsonapi/node/article', [
+      'query' => ['filter' => $filter, 'include' => 'uid,field_tags'],
+    ]));
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertGreaterThan(0, count($single_output['data']));
+    // 15. Test filtering when using invalid syntax.
+    $filter = [
+      'and_group' => ['group' => ['conjunction' => 'AND']],
+      'filter_user' => [
+        'condition' => [
+          'name-with-a-typo' => 'uid.uuid',
+          'value' => $this->user->uuid(),
+          'memberOf' => 'and_group',
+        ],
+      ],
+    ];
+    $this->drupalGet('/jsonapi/node/article', [
+      'query' => ['filter' => $filter],
+    ]);
+    $this->assertSession()->statusCodeEquals(400);
+    // 16. Test filtering on the same field.
+    $filter = [
+      'or_group' => ['group' => ['conjunction' => 'OR']],
+      'filter_tags_1' => [
+        'condition' => [
+          'path' => 'field_tags.uuid',
+          'value' => $this->tags[0]->uuid(),
+          'memberOf' => 'or_group',
+        ],
+      ],
+      'filter_tags_2' => [
+        'condition' => [
+          'path' => 'field_tags.uuid',
+          'value' => $this->tags[1]->uuid(),
+          'memberOf' => 'or_group',
+        ],
+      ],
+    ];
+    $single_output = Json::decode($this->drupalGet('/jsonapi/node/article', [
+      'query' => ['filter' => $filter, 'include' => 'field_tags'],
+    ]));
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertGreaterThanOrEqual(2, count($single_output['included']));
+    // 17. Single user (check fields lacking 'view' access).
+    $user_url = Url::fromRoute('jsonapi.user--user.individual', [
+      'user' => $this->user->uuid(),
+    ]);
+    $response = $this->request('GET', $user_url, [
+      'auth' => [
+        $this->userCanViewProfiles->getUsername(),
+        $this->userCanViewProfiles->pass_raw,
+      ],
+    ]);
+    $single_output = Json::decode($response->getBody()->__toString());
+    $this->assertEquals(200, $response->getStatusCode());
+    $this->assertEquals('user--user', $single_output['data']['type']);
+    $this->assertEquals($this->user->get('name')->value, $single_output['data']['attributes']['name']);
+    $this->assertTrue(empty($single_output['data']['attributes']['mail']));
+    $this->assertTrue(empty($single_output['data']['attributes']['pass']));
+    // 18. Test filtering on the column of a link.
+    $filter = [
+      'linkUri' => [
+        'condition' => [
+          'path' => 'field_link.uri',
+          'value' => 'https://',
+          'operator' => 'STARTS_WITH',
+        ],
+      ],
+    ];
+    $single_output = Json::decode($this->drupalGet('/jsonapi/node/article', [
+      'query' => ['filter' => $filter],
+    ]));
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertGreaterThanOrEqual(1, count($single_output['data']));
+    // 19. Test non-existing route without 'Accept' header.
+    $this->drupalGet('/jsonapi/node/article/broccoli');
+    $this->assertSession()->statusCodeEquals(404);
+    // Without the 'Accept' header we cannot know we want the 404 error
+    // formatted as JSON API.
+    $this->assertSession()->responseHeaderContains('Content-Type', 'text/html');
+    // 20. Test non-existing route with 'Accept' header.
+    $single_output = Json::decode($this->drupalGet('/jsonapi/node/article/broccoli', [], [
+      'Accept' => 'application/vnd.api+json',
+    ]));
+    $this->assertEquals(404, $single_output['errors'][0]['status']);
+    $this->assertSession()->statusCodeEquals(404);
+    // With the 'Accept' header we can know we want the 404 error formatted as
+    // JSON API.
+    $this->assertSession()->responseHeaderContains('Content-Type', 'application/vnd.api+json');
+  }
+
+  /**
+   * Test POST, PATCH and DELETE.
+   */
+  public function testWrite() {
+    $this->createDefaultContent(0, 3, FALSE, FALSE, static::IS_NOT_MULTILINGUAL);
+    // 1. Successful post.
+    $collection_url = Url::fromRoute('jsonapi.node--article.collection');
+    $body = [
+      'data' => [
+        'type' => 'node--article',
+        'attributes' => [
+          'langcode' => 'en',
+          'title' => 'My custom title',
+          'status' => '1',
+          'promote' => '1',
+          'sticky' => '0',
+          'default_langcode' => '1',
+          'body' => [
+            'value' => 'Custom value',
+            'format' => 'plain_text',
+            'summary' => 'Custom summary',
+          ],
+        ],
+        'relationships' => [
+          'type' => [
+            'data' => [
+              'type' => 'node_type--node_type',
+              'id' => 'article',
+            ],
+          ],
+          'uid' => [
+            'data' => [
+              'type' => 'user--user',
+              'id' => '1',
+            ],
+          ],
+          'field_tags' => [
+            'data' => [
+              [
+                'type' => 'taxonomy_term--tags',
+                'id' => $this->tags[0]->uuid(),
+              ],
+              [
+                'type' => 'taxonomy_term--tags',
+                'id' => $this->tags[1]->uuid(),
+              ],
+            ],
+          ],
+        ],
+      ],
+    ];
+    $response = $this->request('POST', $collection_url, [
+      'body' => Json::encode($body),
+      'auth' => [$this->user->getUsername(), $this->user->pass_raw],
+      'headers' => ['Content-Type' => 'application/vnd.api+json'],
+    ]);
+    $created_response = Json::decode($response->getBody()->__toString());
+    $this->assertEquals(201, $response->getStatusCode());
+    $this->assertArrayHasKey('uuid', $created_response['data']['attributes']);
+    $uuid = $created_response['data']['attributes']['uuid'];
+    $this->assertEquals(2, count($created_response['data']['relationships']['field_tags']['data']));
+    $this->assertEquals($created_response['data']['links']['self'], $response->getHeader('Location')[0]);
+
+    // 2. Authorization error.
+    $response = $this->request('POST', $collection_url, [
+      'body' => Json::encode($body),
+      'headers' => ['Content-Type' => 'application/vnd.api+json'],
+    ]);
+    $created_response = Json::decode($response->getBody()->__toString());
+    $this->assertEquals(403, $response->getStatusCode());
+    $this->assertNotEmpty($created_response['errors']);
+    $this->assertEquals('Forbidden', $created_response['errors'][0]['title']);
+
+    // 2.1 Authorization error with a user without create permissions.
+    $response = $this->request('POST', $collection_url, [
+      'body' => Json::encode($body),
+      'auth' => [$this->userCanViewProfiles->getUsername(), $this->userCanViewProfiles->pass_raw],
+      'headers' => ['Content-Type' => 'application/vnd.api+json'],
+    ]);
+    $created_response = Json::decode($response->getBody()->__toString());
+    $this->assertEquals(403, $response->getStatusCode());
+    $this->assertNotEmpty($created_response['errors']);
+    $this->assertEquals('Forbidden', $created_response['errors'][0]['title']);
+
+    // 3. Missing Content-Type error.
+    $response = $this->request('POST', $collection_url, [
+      'body' => Json::encode($body),
+      'auth' => [$this->user->getUsername(), $this->user->pass_raw],
+      'headers' => ['Accept' => 'application/vnd.api+json'],
+    ]);
+    $created_response = Json::decode($response->getBody()->__toString());
+    $this->assertEquals(422, $response->getStatusCode());
+    $this->assertNotEmpty($created_response['errors']);
+    $this->assertEquals('Unprocessable Entity', $created_response['errors'][0]['title']);
+    // 4. Article with a duplicate ID.
+    $invalid_body = $body;
+    $invalid_body['data']['attributes']['nid'] = 1;
+    $response = $this->request('POST', $collection_url, [
+      'body' => Json::encode($invalid_body),
+      'auth' => [$this->user->getUsername(), $this->user->pass_raw],
+      'headers' => ['Content-Type' => 'application/vnd.api+json'],
+    ]);
+    $created_response = Json::decode($response->getBody()->__toString());
+    $this->assertEquals(500, $response->getStatusCode());
+    $this->assertNotEmpty($created_response['errors']);
+    $this->assertEquals('Internal Server Error', $created_response['errors'][0]['title']);
+    // 5. Article with wrong reference UUIDs for tags.
+    $body_invalid_tags = $body;
+    $body_invalid_tags['data']['relationships']['field_tags']['data'][0]['id'] = 'lorem';
+    $body_invalid_tags['data']['relationships']['field_tags']['data'][1]['id'] = 'ipsum';
+    $response = $this->request('POST', $collection_url, [
+      'body' => Json::encode($body_invalid_tags),
+      'auth' => [$this->user->getUsername(), $this->user->pass_raw],
+      'headers' => ['Content-Type' => 'application/vnd.api+json'],
+    ]);
+    $created_response = Json::decode($response->getBody()->__toString());
+    $this->assertEquals(201, $response->getStatusCode());
+    $this->assertEquals(0, count($created_response['data']['relationships']['field_tags']['data']));
+    // 6. Serialization error.
+    $response = $this->request('POST', $collection_url, [
+      'body' => '{"bad json",,,}',
+      'auth' => [$this->user->getUsername(), $this->user->pass_raw],
+      'headers' => [
+        'Content-Type' => 'application/vnd.api+json',
+        'Accept' => 'application/vnd.api+json',
+      ],
+    ]);
+    $created_response = Json::decode($response->getBody()->__toString());
+    $this->assertEquals(422, $response->getStatusCode());
+    $this->assertNotEmpty($created_response['errors']);
+    $this->assertEquals('Unprocessable Entity', $created_response['errors'][0]['title']);
+    // 7. Successful PATCH.
+    $body = [
+      'data' => [
+        'id' => $uuid,
+        'type' => 'node--article',
+        'attributes' => ['title' => 'My updated title'],
+      ],
+    ];
+    $individual_url = Url::fromRoute('jsonapi.node--article.individual', [
+      'node' => $uuid,
+    ]);
+    $response = $this->request('PATCH', $individual_url, [
+      'body' => Json::encode($body),
+      'auth' => [$this->user->getUsername(), $this->user->pass_raw],
+      'headers' => ['Content-Type' => 'application/vnd.api+json'],
+    ]);
+    $updated_response = Json::decode($response->getBody()->__toString());
+    $this->assertEquals(200, $response->getStatusCode());
+    $this->assertEquals('My updated title', $updated_response['data']['attributes']['title']);
+
+    // 7.1 Unsuccessful PATCH due to access restrictions.
+    $body = [
+      'data' => [
+        'id' => $uuid,
+        'type' => 'node--article',
+        'attributes' => ['title' => 'My updated title'],
+      ],
+    ];
+    $individual_url = Url::fromRoute('jsonapi.node--article.individual', [
+      'node' => $uuid,
+    ]);
+    $response = $this->request('PATCH', $individual_url, [
+      'body' => Json::encode($body),
+      'auth' => [$this->userCanViewProfiles->getUsername(), $this->userCanViewProfiles->pass_raw],
+      'headers' => ['Content-Type' => 'application/vnd.api+json'],
+    ]);
+    $this->assertEquals(403, $response->getStatusCode());
+
+    // 8. Field access forbidden check.
+    $body = [
+      'data' => [
+        'id' => $uuid,
+        'type' => 'node--article',
+        'attributes' => [
+          'title' => 'My updated title',
+          'status' => 0,
+        ],
+      ],
+    ];
+    $response = $this->request('PATCH', $individual_url, [
+      'body' => Json::encode($body),
+      'auth' => [$this->user->getUsername(), $this->user->pass_raw],
+      'headers' => ['Content-Type' => 'application/vnd.api+json'],
+    ]);
+    $updated_response = Json::decode($response->getBody()->__toString());
+    $this->assertEquals(403, $response->getStatusCode());
+    $this->assertEquals("The current user is not allowed to PATCH the selected field (status). The 'administer nodes' permission is required.",
+      $updated_response['errors'][0]['detail']);
+
+    $node = \Drupal::entityManager()->loadEntityByUuid('node', $uuid);
+    $this->assertEquals(1, $node->get('status')->value, 'Node status was not changed.');
+    // 9. Successful POST to related endpoint.
+    $body = [
+      'data' => [
+        [
+          'id' => $this->tags[2]->uuid(),
+          'type' => 'taxonomy_term--tags',
+        ],
+      ],
+    ];
+    $relationship_url = Url::fromRoute('jsonapi.node--article.relationship', [
+      'node' => $uuid,
+      'related' => 'field_tags',
+    ]);
+    $response = $this->request('POST', $relationship_url, [
+      'body' => Json::encode($body),
+      'auth' => [$this->user->getUsername(), $this->user->pass_raw],
+      'headers' => ['Content-Type' => 'application/vnd.api+json'],
+    ]);
+    $updated_response = Json::decode($response->getBody()->__toString());
+    $this->assertEquals(201, $response->getStatusCode());
+    $this->assertEquals(3, count($updated_response['data']));
+    $this->assertEquals('taxonomy_term--tags', $updated_response['data'][2]['type']);
+    $this->assertEquals($this->tags[2]->uuid(), $updated_response['data'][2]['id']);
+    // 10. Successful PATCH to related endpoint.
+    $body = [
+      'data' => [
+        [
+          'id' => $this->tags[1]->uuid(),
+          'type' => 'taxonomy_term--tags',
+        ],
+      ],
+    ];
+    $response = $this->request('PATCH', $relationship_url, [
+      'body' => Json::encode($body),
+      'auth' => [$this->user->getUsername(), $this->user->pass_raw],
+      'headers' => ['Content-Type' => 'application/vnd.api+json'],
+    ]);
+    $updated_response = Json::decode($response->getBody()->__toString());
+    $this->assertEquals(200, $response->getStatusCode());
+    $this->assertCount(1, $updated_response['data']);
+    $this->assertEquals('taxonomy_term--tags', $updated_response['data'][0]['type']);
+    $this->assertEquals($this->tags[1]->uuid(), $updated_response['data'][0]['id']);
+    // 11. Successful DELETE to related endpoint.
+    $payload = $updated_response;
+    $response = $this->request('DELETE', $relationship_url, [
+      // Send a request with no body.
+      'auth' => [$this->user->getUsername(), $this->user->pass_raw],
+      'headers' => [
+        'Content-Type' => 'application/vnd.api+json',
+        'Accept' => 'application/vnd.api+json',
+      ],
+    ]);
+    $updated_response = Json::decode($response->getBody()->__toString());
+    $this->assertEquals(
+      'You need to provide a body for DELETE operations on a relationship (field_tags).',
+      $updated_response['errors'][0]['detail']
+    );
+    $this->assertEquals(400, $response->getStatusCode());
+    $response = $this->request('DELETE', $relationship_url, [
+      // Send a request with no authentication.
+      'body' => Json::encode($payload),
+      'headers' => ['Content-Type' => 'application/vnd.api+json'],
+    ]);
+    $this->assertEquals(403, $response->getStatusCode());
+    $response = $this->request('DELETE', $relationship_url, [
+      // Remove the existing relationship item.
+      'body' => Json::encode($payload),
+      'auth' => [$this->user->getUsername(), $this->user->pass_raw],
+      'headers' => ['Content-Type' => 'application/vnd.api+json'],
+    ]);
+    $updated_response = Json::decode($response->getBody()->__toString());
+    $this->assertEquals(201, $response->getStatusCode());
+    $this->assertCount(0, $updated_response['data']);
+    // 12. PATCH with invalid title and body format.
+    $body = [
+      'data' => [
+        'id' => $uuid,
+        'type' => 'node--article',
+        'attributes' => [
+          'title' => '',
+          'body' => [
+            'value' => 'Custom value',
+            'format' => 'invalid_format',
+            'summary' => 'Custom summary',
+          ],
+        ],
+      ],
+    ];
+    $response = $this->request('PATCH', $individual_url, [
+      'body' => Json::encode($body),
+      'auth' => [$this->user->getUsername(), $this->user->pass_raw],
+      'headers' => [
+        'Content-Type' => 'application/vnd.api+json',
+        'Accept' => 'application/vnd.api+json',
+      ],
+    ]);
+    $updated_response = Json::decode($response->getBody()->__toString());
+    $this->assertEquals(422, $response->getStatusCode());
+    $this->assertCount(2, $updated_response['errors']);
+    for ($i = 0; $i < 2; $i++) {
+      $this->assertEquals("Unprocessable Entity", $updated_response['errors'][$i]['title']);
+      $this->assertEquals(422, $updated_response['errors'][$i]['status']);
+      $this->assertEquals(0, $updated_response['errors'][$i]['code']);
+    }
+    $this->assertEquals("title: This value should not be null.", $updated_response['errors'][0]['detail']);
+    $this->assertEquals("body.0.format: The value you selected is not a valid choice.", $updated_response['errors'][1]['detail']);
+    $this->assertEquals("/data/attributes/title", $updated_response['errors'][0]['source']['pointer']);
+    $this->assertEquals("/data/attributes/body/format", $updated_response['errors'][1]['source']['pointer']);
+    // 13. PATCH with field that doesn't exist on Entity.
+    $body = [
+      'data' => [
+        'id' => $uuid,
+        'type' => 'node--article',
+        'attributes' => [
+          'field_that_doesnt_exist' => 'foobar',
+        ],
+      ],
+    ];
+    $response = $this->request('PATCH', $individual_url, [
+      'body' => Json::encode($body),
+      'auth' => [$this->user->getUsername(), $this->user->pass_raw],
+      'headers' => [
+        'Content-Type' => 'application/vnd.api+json',
+        'Accept' => 'application/vnd.api+json',
+      ],
+    ]);
+    $updated_response = Json::decode($response->getBody()->__toString());
+    $this->assertEquals(400, $response->getStatusCode());
+    $this->assertEquals("The provided field (field_that_doesnt_exist) does not exist in the entity with ID $uuid.",
+      $updated_response['errors']['0']['detail']);
+    // 14. Successful DELETE.
+    $response = $this->request('DELETE', $individual_url, [
+      'auth' => [$this->user->getUsername(), $this->user->pass_raw],
+    ]);
+    $this->assertEquals(204, $response->getStatusCode());
+    $response = $this->request('GET', $individual_url, []);
+    $this->assertEquals(404, $response->getStatusCode());
+  }
+
+}
diff --git a/drupal/modules/jsonapi/tests/src/Functional/JsonApiFunctionalTestBase.php b/drupal/modules/jsonapi/tests/src/Functional/JsonApiFunctionalTestBase.php
new file mode 100644
index 0000000..1937151
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Functional/JsonApiFunctionalTestBase.php
@@ -0,0 +1,267 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Functional;
+
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Url;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\field\Tests\EntityReference\EntityReferenceTestTrait;
+use Drupal\file\Entity\File;
+use Drupal\taxonomy\Entity\Term;
+use Drupal\taxonomy\Entity\Vocabulary;
+use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\image\Kernel\ImageFieldCreationTrait;
+use Drupal\user\Entity\Role;
+use Drupal\user\RoleInterface;
+use GuzzleHttp\Exception\ClientException;
+use GuzzleHttp\Exception\ServerException;
+
+/**
+ * Provides helper methods for the JSON API module's functional tests.
+ */
+abstract class JsonApiFunctionalTestBase extends BrowserTestBase {
+
+  use EntityReferenceTestTrait;
+  use ImageFieldCreationTrait;
+
+  const IS_MULTILINGUAL = TRUE;
+  const IS_NOT_MULTILINGUAL = FALSE;
+
+  public static $modules = [
+    'basic_auth',
+    'jsonapi',
+    'serialization',
+    'node',
+    'image',
+    'taxonomy',
+    'link',
+  ];
+
+  /**
+   * @var \Drupal\user\Entity\User
+   */
+  protected $user;
+
+  /**
+   * @var \Drupal\user\Entity\User
+   */
+  protected $userCanViewProfiles;
+
+  /**
+   * @var \Drupal\node\Entity\Node[]
+   */
+  protected $nodes = [];
+
+  /**
+   * @var \Drupal\taxonomy\Entity\Term[]
+   */
+  protected $tags = [];
+
+  /**
+   * @var \Drupal\file\Entity\File[]
+   */
+  protected $files = [];
+
+  /**
+   * @var \GuzzleHttp\ClientInterface
+   */
+  protected $httpClient;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    // Set up a HTTP client that accepts relative URLs.
+    $this->httpClient = $this->container->get('http_client_factory')
+      ->fromOptions(['base_uri' => $this->baseUrl]);
+
+    // Create Basic page and Article node types.
+    if ($this->profile != 'standard') {
+      $this->drupalCreateContentType([
+        'type' => 'article',
+        'name' => 'Article',
+      ]);
+
+      // Setup vocabulary.
+      Vocabulary::create([
+        'vid' => 'tags',
+        'name' => 'Tags',
+      ])->save();
+
+      // Add tags and field_image to the article.
+      $this->createEntityReferenceField(
+        'node',
+        'article',
+        'field_tags',
+        'Tags',
+        'taxonomy_term',
+        'default',
+        [
+          'target_bundles' => [
+            'tags' => 'tags',
+          ],
+          'auto_create' => TRUE,
+        ],
+        FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
+      );
+      $this->createImageField('field_image', 'article');
+    }
+
+    FieldStorageConfig::create([
+      'field_name' => 'field_link',
+      'entity_type' => 'node',
+      'type' => 'link',
+      'settings' => [],
+      'cardinality' => 1,
+    ])->save();
+
+    $field_config = FieldConfig::create([
+      'field_name' => 'field_link',
+      'label' => 'Link',
+      'entity_type' => 'node',
+      'bundle' => 'article',
+      'required' => FALSE,
+      'settings' => [],
+      'description' => '',
+    ]);
+    $field_config->save();
+
+    $this->user = $this->drupalCreateUser([
+      'create article content',
+      'edit any article content',
+      'delete any article content',
+    ]);
+
+    // Create a user that can.
+    $this->userCanViewProfiles = $this->drupalCreateUser([
+      'access user profiles',
+    ]);
+
+    $this->grantPermissions(Role::load(RoleInterface::ANONYMOUS_ID), [
+      'access user profiles',
+      'administer taxonomy',
+    ]);
+
+    drupal_flush_all_caches();
+  }
+
+  /**
+   * Performs a HTTP request. Wraps the Guzzle HTTP client.
+   *
+   * Why wrap the Guzzle HTTP client? Because any error response is returned via
+   * an exception, which would make the tests unnecessarily complex to read.
+   *
+   * @see \GuzzleHttp\ClientInterface::request()
+   *
+   * @param string $method
+   *   HTTP method.
+   * @param \Drupal\Core\Url $url
+   *   URL to request.
+   * @param array $request_options
+   *   Request options to apply.
+   *
+   * @return \Psr\Http\Message\ResponseInterface
+   */
+  protected function request($method, Url $url, array $request_options) {
+    try {
+      $response = $this->httpClient->request($method, $url->toString(), $request_options);
+    }
+    catch (ClientException $e) {
+      $response = $e->getResponse();
+    }
+    catch (ServerException $e) {
+      $response = $e->getResponse();
+    }
+
+    return $response;
+  }
+
+  /**
+   * Creates default content to test the API.
+   *
+   * @param int $num_articles
+   *   Number of articles to create.
+   * @param int $num_tags
+   *   Number of tags to create.
+   * @param bool $article_has_image
+   *   Set to TRUE if you want to add an image to the generated articles.
+   * @param bool $article_has_link
+   *   Set to TRUE if you want to add a link to the generated articles.
+   * @param bool $is_multilingual
+   *   (optional) Set to TRUE if you want to enable multilingual content.
+   */
+  protected function createDefaultContent($num_articles, $num_tags, $article_has_image, $article_has_link, $is_multilingual) {
+    $random = $this->getRandomGenerator();
+    for ($created_tags = 0; $created_tags < $num_tags; $created_tags++) {
+      $term = Term::create([
+        'vid' => 'tags',
+        'name' => $random->name(),
+      ]);
+
+      if ($is_multilingual) {
+        $term->addTranslation('ca', ['name' => $term->getName() . ' (ca)']);
+      }
+
+      $term->save();
+      $this->tags[] = $term;
+    }
+    for ($created_nodes = 0; $created_nodes < $num_articles; $created_nodes++) {
+      // Get N random tags.
+      $selected_tags = mt_rand(1, $num_tags);
+      $tags = [];
+      while (count($tags) < $selected_tags) {
+        $tags[] = mt_rand(1, $num_tags);
+        $tags = array_unique($tags);
+      }
+      $values = [
+        'uid' => ['target_id' => $this->user->id()],
+        'type' => 'article',
+        'field_tags' => array_map(function ($tag) {
+          return ['target_id' => $tag];
+        }, $tags),
+      ];
+      if ($article_has_image) {
+        $file = File::create([
+          'uri' => 'vfs://' . $random->name() . '.png',
+        ]);
+        $file->setPermanent();
+        $file->save();
+        $this->files[] = $file;
+        $values['field_image'] = ['target_id' => $file->id(), 'alt' => 'alt text'];
+      }
+      if ($article_has_link) {
+        $values['field_link'] = [
+          'title' => $this->getRandomGenerator()->name(),
+          'uri' => sprintf(
+            '%s://%s.%s',
+            'http' . (mt_rand(0, 2) > 1 ? '' : 's'),
+            $this->getRandomGenerator()->name(),
+            'org'
+          ),
+        ];
+      }
+      $node = $this->createNode($values);
+
+      if ($is_multilingual === static::IS_MULTILINGUAL) {
+        $values['title'] = $node->getTitle() . ' (ca)';
+        $values['field_image']['alt'] = 'alt text (ca)';
+        $node->addTranslation('ca', $values);
+      }
+      $node->save();
+
+      $this->nodes[] = $node;
+    }
+    if ($article_has_link) {
+      // Make sure that there is at least 1 https link for ::testRead() #19.
+      $this->nodes[0]->field_link = [
+        'title' => 'Drupal',
+        'uri' => 'https://drupal.org',
+      ];
+      $this->nodes[0]->save();
+    }
+  }
+
+}
diff --git a/drupal/modules/jsonapi/tests/src/Functional/RestJsonApiUnsupported.php b/drupal/modules/jsonapi/tests/src/Functional/RestJsonApiUnsupported.php
new file mode 100644
index 0000000..e58e4f4
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Functional/RestJsonApiUnsupported.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Functional;
+
+use Drupal\Core\Url;
+use Drupal\node\Entity\Node;
+use Drupal\node\Entity\NodeType;
+use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
+use Drupal\Tests\rest\Functional\ResourceTestBase;
+
+/**
+ * @group jsonapi
+ */
+class RestJsonApiUnsupported extends ResourceTestBase {
+
+  use AnonResourceTestTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['jsonapi', 'node'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $format = 'api_json';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $mimeType = 'application/vnd.api+json';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $resourceConfigId = 'entity.node';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUpAuthorization($method) {
+    switch ($method) {
+      case 'GET':
+        $this->grantPermissionsToTestedRole(['access content']);
+        break;
+
+      default:
+        throw new \UnexpectedValueException();
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    // Set up a HTTP client that accepts relative URLs.
+    $this->httpClient = $this->container->get('http_client_factory')
+      ->fromOptions(['base_uri' => $this->baseUrl]);
+
+    // Create a "Camelids" node type.
+    NodeType::create([
+      'name' => 'Camelids',
+      'type' => 'camelids',
+    ])->save();
+
+    // Create a "Llama" node.
+    $node = Node::create(['type' => 'camelids']);
+    $node->setTitle('Llama')
+      ->setOwnerId(0)
+      ->setPublished(TRUE)
+      ->save();
+  }
+
+  /**
+   * Deploying a REST resource using api_json format results in 406 responses.
+   */
+  public function testApiJsonNotSupportedInRest() {
+    $this->assertSame(['json', 'xml'], $this->container->getParameter('serializer.formats'));
+
+    $this->provisionResource(['api_json'], []);
+    $this->setUpAuthorization('GET');
+
+    $url = Node::load(1)->toUrl()
+      ->setOption('query', ['_format' => 'api_json']);
+    $request_options = [];
+
+    $response = $this->request('GET', $url, $request_options);
+    $this->assertResourceErrorResponse(406, FALSE, $response);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function assertNormalizationEdgeCases($method, Url $url, array $request_options) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExpectedUnauthorizedAccessMessage($method) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExpectedBcUnauthorizedAccessMessage($method) {}
+
+}
diff --git a/drupal/modules/jsonapi/tests/src/Kernel/Context/FieldResolverTest.php b/drupal/modules/jsonapi/tests/src/Kernel/Context/FieldResolverTest.php
new file mode 100644
index 0000000..441119e
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Kernel/Context/FieldResolverTest.php
@@ -0,0 +1,199 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Kernel\Context;
+
+use Drupal\entity_test\Entity\EntityTestBundle;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\Tests\jsonapi\Kernel\JsonapiKernelTestBase;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * @coversDefaultClass \Drupal\jsonapi\Context\FieldResolver
+ * @group jsonapi
+ */
+class FieldResolverTest extends JsonapiKernelTestBase {
+
+  public static $modules = ['entity_test', 'serialization', 'field', 'text'];
+
+  /**
+   * @var \Drupal\jsonapi\Context\FieldResolver
+   */
+  protected $sut;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->sut = \Drupal::service('jsonapi.field_resolver');
+
+    EntityTestBundle::create([
+      'id' => 'bundle1',
+    ])->save();
+    EntityTestBundle::create([
+      'id' => 'bundle2',
+    ])->save();
+    EntityTestBundle::create([
+      'id' => 'bundle3',
+    ])->save();
+
+    FieldStorageConfig::create([
+      'field_name' => 'field_test1',
+      'type' => 'string',
+      'entity_type' => 'entity_test_with_bundle',
+    ])->save();
+    FieldConfig::create([
+      'field_name' => 'field_test1',
+      'entity_type' => 'entity_test_with_bundle',
+      'bundle' => 'bundle1',
+    ])->save();
+
+    FieldStorageConfig::create([
+      'field_name' => 'field_test2',
+      'type' => 'string',
+      'entity_type' => 'entity_test_with_bundle',
+    ])->save();
+    FieldConfig::create([
+      'field_name' => 'field_test2',
+      'entity_type' => 'entity_test_with_bundle',
+      'bundle' => 'bundle1',
+    ])->save();
+
+    FieldStorageConfig::create([
+      'field_name' => 'field_test3',
+      'type' => 'string',
+      'entity_type' => 'entity_test_with_bundle',
+    ])->save();
+    FieldConfig::create([
+      'field_name' => 'field_test3',
+      'entity_type' => 'entity_test_with_bundle',
+      'bundle' => 'bundle2',
+    ])->save();
+    FieldConfig::create([
+      'field_name' => 'field_test3',
+      'entity_type' => 'entity_test_with_bundle',
+      'bundle' => 'bundle3',
+    ])->save();
+
+    // Provides entity reference fields.
+    FieldStorageConfig::create([
+      'field_name' => 'field_test_ref1',
+      'type' => 'entity_reference',
+      'settings' => [
+        'target_type' => 'entity_test_with_bundle',
+      ],
+      'entity_type' => 'entity_test_with_bundle',
+    ])->save();
+    FieldConfig::create([
+      'field_name' => 'field_test_ref1',
+      'entity_type' => 'entity_test_with_bundle',
+      'bundle' => 'bundle1',
+    ])->save();
+
+    FieldStorageConfig::create([
+      'field_name' => 'field_test_ref2',
+      'type' => 'entity_reference',
+      'settings' => [
+        'target_type' => 'entity_test_with_bundle',
+      ],
+      'entity_type' => 'entity_test_with_bundle',
+    ])->save();
+    FieldConfig::create([
+      'field_name' => 'field_test_ref2',
+      'entity_type' => 'entity_test_with_bundle',
+      'bundle' => 'bundle1',
+    ])->save();
+
+    FieldStorageConfig::create([
+      'field_name' => 'field_test_ref3',
+      'type' => 'entity_reference',
+      'settings' => [
+        'target_type' => 'entity_test_with_bundle',
+      ],
+      'entity_type' => 'entity_test_with_bundle',
+    ])->save();
+    FieldConfig::create([
+      'field_name' => 'field_test_ref3',
+      'entity_type' => 'entity_test_with_bundle',
+      'bundle' => 'bundle2',
+    ])->save();
+    FieldConfig::create([
+      'field_name' => 'field_test_ref3',
+      'entity_type' => 'entity_test_with_bundle',
+      'bundle' => 'bundle3',
+    ])->save();
+
+    // Add a field with multiple properties.
+    FieldStorageConfig::create([
+      'field_name' => 'field_test_text',
+      'type' => 'text',
+      'entity_type' => 'entity_test_with_bundle',
+    ])->save();
+    FieldConfig::create([
+      'field_name' => 'field_test_text',
+      'entity_type' => 'entity_test_with_bundle',
+      'bundle' => 'bundle1',
+    ])->save();
+    FieldConfig::create([
+      'field_name' => 'field_test_text',
+      'entity_type' => 'entity_test_with_bundle',
+      'bundle' => 'bundle2',
+    ])->save();
+  }
+
+  public function testResolveInternal() {
+    $request = Request::create('/jsonapi/entity_test_with_bundle/bundle1');
+    $route = \Drupal::service('router.route_provider')->getRouteByName('jsonapi.entity_test_with_bundle--bundle1.collection');
+    $request->attributes->set(RouteObjectInterface::ROUTE_NAME, 'jsonapi.entity_test_with_bundle--bundle1.collection');
+    $request->attributes->set(RouteObjectInterface::ROUTE_OBJECT, $route);
+
+    \Drupal::requestStack()->push($request);
+
+    $this->assertEquals('field_test1', $this->sut->resolveInternal('field_test1'));
+    $this->assertEquals('field_test2', $this->sut->resolveInternal('field_test2'));
+    $this->assertEquals('field_test3', $this->sut->resolveInternal('field_test3'));
+
+    $this->assertEquals('field_test_ref1.entity.field_test1', $this->sut->resolveInternal('field_test_ref1.field_test1'));
+    $this->assertEquals('field_test_ref1.entity.field_test2', $this->sut->resolveInternal('field_test_ref1.field_test2'));
+    $this->assertEquals('field_test_ref2.entity.field_test1', $this->sut->resolveInternal('field_test_ref2.field_test1'));
+    $this->assertEquals('field_test_ref2.entity.field_test2', $this->sut->resolveInternal('field_test_ref2.field_test2'));
+    $this->assertEquals('field_test_ref3.entity.field_test1', $this->sut->resolveInternal('field_test_ref3.field_test1'));
+    $this->assertEquals('field_test_ref3.entity.field_test2', $this->sut->resolveInternal('field_test_ref3.field_test2'));
+
+    $this->assertEquals('field_test_ref1.entity.field_test_text', $this->sut->resolveInternal('field_test_ref1.field_test_text'));
+    $this->assertEquals('field_test_ref1.entity.field_test_text.value', $this->sut->resolveInternal('field_test_ref1.field_test_text.value'));
+    $this->assertEquals('field_test_ref1.entity.field_test_text.format', $this->sut->resolveInternal('field_test_ref1.field_test_text.format'));
+    $this->assertEquals('field_test_ref2.entity.field_test_text', $this->sut->resolveInternal('field_test_ref2.field_test_text'));
+    $this->assertEquals('field_test_ref2.entity.field_test_text.value', $this->sut->resolveInternal('field_test_ref2.field_test_text.value'));
+    $this->assertEquals('field_test_ref2.entity.field_test_text.format', $this->sut->resolveInternal('field_test_ref2.field_test_text.format'));
+  }
+
+  /**
+   * Expects an error when an invalid field is provided.
+   *
+   * @covers ::resolveInternal
+   *
+   * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
+   */
+  public function testResolveInternalError() {
+    $request = Request::create('/jsonapi/entity_test_with_bundle/bundle1');
+    $route = \Drupal::service('router.route_provider')
+      ->getRouteByName('jsonapi.entity_test_with_bundle--bundle1.collection');
+    $request->attributes->set(
+      RouteObjectInterface::ROUTE_NAME,
+      'jsonapi.entity_test_with_bundle--bundle1.collection'
+    );
+    $request->attributes->set(RouteObjectInterface::ROUTE_OBJECT, $route);
+
+    \Drupal::requestStack()->push($request);
+
+    $original = 'host.fail!!.deep';
+    $not_expected = 'host.entity.fail!!.entity.deep';
+
+    $this->assertEquals($not_expected, $this->sut->resolveInternal($original));
+  }
+
+}
diff --git a/drupal/modules/jsonapi/tests/src/Kernel/Controller/EntityResourceTest.php b/drupal/modules/jsonapi/tests/src/Kernel/Controller/EntityResourceTest.php
new file mode 100644
index 0000000..ae8aeef
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Kernel/Controller/EntityResourceTest.php
@@ -0,0 +1,880 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Kernel\Controller;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Routing\CurrentRouteMatch;
+use Drupal\jsonapi\ResourceType\ResourceType;
+use Drupal\jsonapi\Context\CurrentContext;
+use Drupal\jsonapi\Controller\EntityResource;
+use Drupal\jsonapi\Resource\EntityCollection;
+use Drupal\jsonapi\Resource\JsonApiDocumentTopLevel;
+use Drupal\jsonapi\Routing\Param\Filter;
+use Drupal\jsonapi\Routing\Param\Sort;
+use Drupal\jsonapi\Routing\Param\OffsetPage;
+use Drupal\node\Entity\Node;
+use Drupal\node\Entity\NodeType;
+use Drupal\Tests\jsonapi\Kernel\JsonapiKernelTestBase;
+use Drupal\user\Entity\Role;
+use Drupal\user\Entity\User;
+use Drupal\user\RoleInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
+use Symfony\Component\HttpKernel\Exception\HttpException;
+use Symfony\Component\Routing\Route;
+
+/**
+ * @coversDefaultClass \Drupal\jsonapi\Controller\EntityResource
+ * @group jsonapi
+ */
+class EntityResourceTest extends JsonapiKernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'node',
+    'field',
+    'jsonapi',
+    'serialization',
+    'system',
+    'user',
+  ];
+
+  /**
+   * The user.
+   *
+   * @var \Drupal\user\Entity\User
+   */
+  protected $user;
+
+  /**
+   * The node.
+   *
+   * @var \Drupal\node\Entity\Node
+   */
+  protected $node;
+
+  /**
+   * The other node.
+   *
+   * @var \Drupal\node\Entity\Node
+   */
+  protected $node2;
+
+  /**
+   * An unpublished node.
+   *
+   * @var \Drupal\node\Entity\Node
+   */
+  protected $node3;
+
+  /**
+   * A fake request.
+   *
+   * @var \Symfony\Component\HttpFoundation\Request
+   */
+  protected $request;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    // Add the entity schemas.
+    $this->installEntitySchema('node');
+    $this->installEntitySchema('user');
+    // Add the additional table schemas.
+    $this->installSchema('system', ['sequences']);
+    $this->installSchema('node', ['node_access']);
+    $this->installSchema('user', ['users_data']);
+    NodeType::create([
+      'type' => 'lorem',
+    ])->save();
+    $type = NodeType::create([
+      'type' => 'article',
+    ]);
+    $type->save();
+    $this->user = User::create([
+      'name' => 'user1',
+      'mail' => 'user@localhost',
+      'status' => 1,
+      'roles' => ['test_role_one', 'test_role_two'],
+    ]);
+    $this->createEntityReferenceField('node', 'article', 'field_relationships', 'Relationship', 'node', 'default', ['target_bundles' => ['article']], FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
+    $this->user->save();
+    $this->node = Node::create([
+      'title' => 'dummy_title',
+      'type' => 'article',
+      'uid' => $this->user->id(),
+    ]);
+    $this->node->save();
+
+    $this->node2 = Node::create([
+      'type' => 'article',
+      'title' => 'Another test node',
+      'uid' => $this->user->id(),
+    ]);
+    $this->node2->save();
+
+    $this->node3 = Node::create([
+      'type' => 'article',
+      'title' => 'Unpublished test node',
+      'uid' => $this->user->id(),
+      'status' => 0,
+    ]);
+    $this->node3->save();
+
+    $this->node4 = Node::create([
+      'type' => 'article',
+      'title' => 'Test node with related nodes',
+      'uid' => $this->user->id(),
+      'field_relationships' => [
+        ['target_id' => $this->node->id()],
+        ['target_id' => $this->node2->id()],
+        ['target_id' => $this->node3->id()],
+      ],
+    ]);
+    $this->node4->save();
+
+    // Give anonymous users permission to view user profiles, so that we can
+    // verify the cache tags of cached versions of user profile pages.
+    array_map(function ($role_id) {
+      Role::create([
+        'id' => $role_id,
+        'permissions' => [
+          'access user profiles',
+          'access content',
+        ],
+      ])->save();
+    }, [RoleInterface::ANONYMOUS_ID, 'test_role_one', 'test_role_two']);
+  }
+
+  /**
+   * @covers ::getIndividual
+   */
+  public function testGetIndividual() {
+    $entity_resource = $this->buildEntityResource('node', 'article');
+    $response = $entity_resource->getIndividual($this->node, new Request());
+    $this->assertInstanceOf(JsonApiDocumentTopLevel::class, $response->getResponseData());
+    $this->assertEquals(1, $response->getResponseData()->getData()->id());
+  }
+
+  /**
+   * @covers ::getIndividual
+   * @expectedException \Drupal\jsonapi\Exception\EntityAccessDeniedHttpException
+   */
+  public function testGetIndividualDenied() {
+    $role = Role::load(RoleInterface::ANONYMOUS_ID);
+    $role->revokePermission('access content');
+    $role->save();
+    $entity_resource = $this->buildEntityResource('node', 'article');
+    $entity_resource->getIndividual($this->node, new Request());
+  }
+
+  /**
+   * @covers ::getCollection
+   */
+  public function testGetCollection() {
+    $request = new Request([], [], [
+      '_route_params' => ['_json_api_params' => []],
+      '_json_api_params' => [],
+    ]);
+
+    // Get the response.
+    $entity_resource = $this->buildEntityResource('node', 'article');
+    $response = $entity_resource->getCollection($request);
+
+    // Assertions.
+    $this->assertInstanceOf(JsonApiDocumentTopLevel::class, $response->getResponseData());
+    $this->assertInstanceOf(EntityCollection::class, $response->getResponseData()->getData());
+    $this->assertEquals(1, $response->getResponseData()->getData()->getIterator()->current()->id());
+    $this->assertEquals([
+      'node:1',
+      'node:2',
+      'node:3',
+      'node:4',
+      'node_list',
+    ], $response->getCacheableMetadata()->getCacheTags());
+  }
+
+  /**
+   * @covers ::getCollection
+   */
+  public function testGetFilteredCollection() {
+    $field_manager = $this->container->get('entity_field.manager');
+    $filter = new Filter(['type' => ['value' => 'article']]);
+    // The fake route.
+    $route = new Route(NULL, [], [
+      '_entity_type' => 'node',
+      '_bundle' => 'article',
+    ]);
+    // The request.
+    $request = new Request([], [], [
+      '_route_params' => [
+        '_json_api_params' => [
+          'filter' => $filter,
+        ],
+      ],
+      '_json_api_params' => [
+        'filter' => $filter,
+      ],
+      '_route_object' => $route,
+    ]);
+    $request_stack = new RequestStack();
+    $request_stack->push($request);
+    // Get the entity resource.
+    $current_context = new CurrentContext(
+      $this->container->get('jsonapi.resource_type.repository'),
+      $request_stack,
+      new CurrentRouteMatch($request_stack)
+    );
+    $this->container->set('jsonapi.current_context', $current_context);
+
+    $entity_resource = new EntityResource(
+      $this->container->get('jsonapi.resource_type.repository')->get('node_type', 'node_type'),
+      $this->container->get('entity_type.manager'),
+      $this->container->get('jsonapi.query_builder'),
+      $field_manager,
+      $current_context,
+      $this->container->get('plugin.manager.field.field_type'),
+      $this->container->get('jsonapi.link_manager')
+    );
+
+    // Get the response.
+    $response = $entity_resource->getCollection($request);
+
+    // Assertions.
+    $this->assertInstanceOf(JsonApiDocumentTopLevel::class, $response->getResponseData());
+    $this->assertInstanceOf(EntityCollection::class, $response->getResponseData()->getData());
+    $this->assertCount(1, $response->getResponseData()->getData());
+    $this->assertEquals(['config:node_type_list'], $response->getCacheableMetadata()->getCacheTags());
+  }
+
+  /**
+   * @covers ::getCollection
+   */
+  public function testGetSortedCollection() {
+    // Fake the request.
+    $field_manager = $this->container->get('entity_field.manager');
+    // The fake route.
+    $route = new Route(NULL, [], [
+      '_entity_type' => 'node',
+      '_bundle' => 'article',
+    ]);
+    $sort = new Sort('-type');
+    // The request.
+    $request = new Request([], [], [
+      '_route_params' => [
+        '_json_api_params' => [
+          'sort' => $sort,
+        ],
+      ],
+      '_json_api_params' => [
+        'sort' => $sort,
+      ],
+      '_route_object' => $route,
+    ]);
+    $request_stack = new RequestStack();
+    $request_stack->push($request);
+    // Get the entity resource.
+    $current_context = new CurrentContext(
+      $this->container->get('jsonapi.resource_type.repository'),
+      $request_stack,
+      new CurrentRouteMatch($request_stack)
+    );
+    $this->container->set('jsonapi.current_context', $current_context);
+
+    $entity_resource = new EntityResource(
+      $this->container->get('jsonapi.resource_type.repository')->get('node_type', 'node_type'),
+      $this->container->get('entity_type.manager'),
+      $this->container->get('jsonapi.query_builder'),
+      $field_manager,
+      $current_context,
+      $this->container->get('plugin.manager.field.field_type'),
+      $this->container->get('jsonapi.link_manager')
+    );
+
+    // Get the response.
+    $response = $entity_resource->getCollection($request);
+
+    // Assertions.
+    $this->assertInstanceOf(JsonApiDocumentTopLevel::class, $response->getResponseData());
+    $this->assertInstanceOf(EntityCollection::class, $response->getResponseData()->getData());
+    $this->assertCount(2, $response->getResponseData()->getData());
+    $this->assertEquals($response->getResponseData()->getData()->toArray()[0]->id(), 'lorem');
+    $this->assertEquals(['config:node_type_list'], $response->getCacheableMetadata()->getCacheTags());
+  }
+
+  /**
+   * @covers ::getCollection
+   */
+  public function testGetPagedCollection() {
+    // Fake the request.
+    $field_manager = $this->container->get('entity_field.manager');
+    // The fake route.
+    $route = new Route(NULL, [], [
+      '_entity_type' => 'node',
+      '_bundle' => 'article',
+    ]);
+    $pager = new OffsetPage(['offset' => 1, 'limit' => 1]);
+    // The request.
+    $request = new Request([], [], [
+      '_route_params' => [
+        '_json_api_params' => [
+          'page' => $pager,
+        ],
+      ],
+      '_json_api_params' => [
+        'page' => $pager,
+      ],
+      '_route_object' => $route,
+    ]);
+    $request_stack = new RequestStack();
+    $request_stack->push($request);
+    // Get the entity resource.
+    $current_context = new CurrentContext(
+      $this->container->get('jsonapi.resource_type.repository'),
+      $request_stack,
+      new CurrentRouteMatch($request_stack)
+    );
+    $this->container->set('jsonapi.current_context', $current_context);
+
+    $entity_resource = new EntityResource(
+      $this->container->get('jsonapi.resource_type.repository')->get('node', 'article'),
+      $this->container->get('entity_type.manager'),
+      $this->container->get('jsonapi.query_builder'),
+      $field_manager,
+      $current_context,
+      $this->container->get('plugin.manager.field.field_type'),
+      $this->container->get('jsonapi.link_manager')
+    );
+
+    // Get the response.
+    $response = $entity_resource->getCollection($request);
+
+    // Assertions.
+    $this->assertInstanceOf(JsonApiDocumentTopLevel::class, $response->getResponseData());
+    $this->assertInstanceOf(EntityCollection::class, $response->getResponseData()->getData());
+    $data = $response->getResponseData()->getData();
+    $this->assertCount(1, $data);
+    $this->assertEquals(2, $data->toArray()[0]->id());
+    $this->assertEquals(['node:2', 'node_list'], $response->getCacheableMetadata()->getCacheTags());
+  }
+
+  /**
+   * @covers ::getCollection
+   */
+  public function testGetEmptyCollection() {
+    $filter = new Filter(['uuid' => ['value' => 'invalid']]);
+    $request = new Request([], [], [
+      '_route_params' => [
+        '_json_api_params' => [
+          'filter' => $filter,
+        ],
+      ],
+      '_json_api_params' => [
+        'filter' => $filter,
+      ],
+    ]);
+
+    // Get the response.
+    $entity_resource = $this->buildEntityResource('node', 'article');
+    $response = $entity_resource->getCollection($request);
+
+    // Assertions.
+    $this->assertInstanceOf(JsonApiDocumentTopLevel::class, $response->getResponseData());
+    $this->assertInstanceOf(EntityCollection::class, $response->getResponseData()->getData());
+    $this->assertEquals(0, $response->getResponseData()->getData()->count());
+    $this->assertEquals(['node_list'], $response->getCacheableMetadata()->getCacheTags());
+  }
+
+  /**
+   * @covers ::getRelated
+   */
+  public function testGetRelated() {
+    // to-one relationship.
+    $entity_resource = $this->buildEntityResource('node', 'article');
+    $response = $entity_resource->getRelated($this->node, 'uid', new Request());
+    $this->assertInstanceOf(JsonApiDocumentTopLevel::class, $response->getResponseData());
+    $this->assertInstanceOf(User::class, $response->getResponseData()
+      ->getData());
+    $this->assertEquals(1, $response->getResponseData()->getData()->id());
+    $this->assertEquals(
+      ['node:1'],
+      $response->getCacheableMetadata()->getCacheTags()
+    );
+    // to-many relationship.
+    $response = $entity_resource->getRelated($this->user, 'roles', new Request());
+    $this->assertInstanceOf(JsonApiDocumentTopLevel::class, $response
+      ->getResponseData());
+    $this->assertInstanceOf(EntityCollection::class, $response
+      ->getResponseData()
+      ->getData());
+    $this->assertEquals([
+      'config:user.role.test_role_one',
+      'config:user.role.test_role_two',
+      'user:1',
+    ], $response
+      ->getCacheableMetadata()
+      ->getCacheTags());
+    // to-many relationship.
+    $response = $entity_resource->getRelated($this->node4, 'field_relationships', new Request());
+    $this->assertInstanceOf(JsonApiDocumentTopLevel::class, $response
+      ->getResponseData());
+    $this->assertInstanceOf(EntityCollection::class, $response
+      ->getResponseData()
+      ->getData());
+    $this->assertEquals(
+      ['node:1', 'node:2', 'node:3', 'node:4'],
+      $response->getCacheableMetadata()->getCacheTags()
+    );
+  }
+
+  /**
+   * @covers ::getRelationship
+   */
+  public function testGetRelationship() {
+    // to-one relationship.
+    $entity_resource = $this->buildEntityResource('node', 'article');
+    $response = $entity_resource->getRelationship($this->node, 'uid', new Request());
+    $this->assertInstanceOf(JsonApiDocumentTopLevel::class, $response->getResponseData());
+    $this->assertInstanceOf(
+      EntityReferenceFieldItemListInterface::class,
+      $response->getResponseData()->getData()
+    );
+    $this->assertEquals(1, $response
+      ->getResponseData()
+      ->getData()
+      ->getEntity()
+      ->id()
+    );
+    $this->assertEquals('node', $response
+      ->getResponseData()
+      ->getData()
+      ->getEntity()
+      ->getEntityTypeId()
+    );
+  }
+
+  /**
+   * @covers ::createIndividual
+   */
+  public function testCreateIndividual() {
+    $node = Node::create([
+      'type' => 'article',
+      'title' => 'Lorem ipsum',
+    ]);
+    Role::load(Role::ANONYMOUS_ID)
+      ->grantPermission('create article content')
+      ->save();
+    $entity_resource = $this->buildEntityResource('node', 'article');
+    $response = $entity_resource->createIndividual($node, new Request());
+    // As a side effect, the node will also be saved.
+    $this->assertNotEmpty($node->id());
+    $this->assertInstanceOf(JsonApiDocumentTopLevel::class, $response->getResponseData());
+    $this->assertEquals(5, $response->getResponseData()->getData()->id());
+    $this->assertEquals(201, $response->getStatusCode());
+  }
+
+  /**
+   * @covers ::createIndividual
+   */
+  public function testCreateIndividualWithMissingRequiredData() {
+    $node = Node::create([
+      'type' => 'article',
+      // No title specified, even if its required.
+    ]);
+    Role::load(Role::ANONYMOUS_ID)
+      ->grantPermission('create article content')
+      ->save();
+    $this->setExpectedException(HttpException::class, 'Unprocessable Entity: validation failed.');
+    $entity_resource = $this->buildEntityResource('node', 'article');
+    $entity_resource->createIndividual($node, new Request());
+  }
+
+  /**
+   * @covers ::createIndividual
+   */
+  public function testCreateIndividualConfig() {
+    $node_type = NodeType::create([
+      'type' => 'test',
+      'name' => 'Test Type',
+      'description' => 'Lorem ipsum',
+    ]);
+    Role::load(Role::ANONYMOUS_ID)
+      ->grantPermission('administer content types')
+      ->save();
+    $entity_resource = $this->buildEntityResource('node', 'article');
+    $response = $entity_resource->createIndividual($node_type, new Request());
+    // As a side effect, the node type will also be saved.
+    $this->assertNotEmpty($node_type->id());
+    $this->assertInstanceOf(JsonApiDocumentTopLevel::class, $response->getResponseData());
+    $this->assertEquals('test', $response->getResponseData()->getData()->id());
+    $this->assertEquals(201, $response->getStatusCode());
+  }
+
+  /**
+   * @covers ::createIndividual
+   */
+  public function testCreateIndividualDuplicateError() {
+    Role::load(Role::ANONYMOUS_ID)
+      ->grantPermission('create article content')
+      ->save();
+
+    $node = Node::create([
+      'type' => 'article',
+      'title' => 'Lorem ipsum',
+    ]);
+    $node->save();
+    $node->enforceIsNew();
+
+    $this->setExpectedException(ConflictHttpException::class, 'Conflict: Entity already exists.');
+    $entity_resource = $this->buildEntityResource('node', 'article');
+    $entity_resource->createIndividual($node, new Request());
+  }
+
+  /**
+   * @covers ::patchIndividual
+   * @dataProvider patchIndividualProvider
+   */
+  public function testPatchIndividual($values) {
+    $parsed_node = Node::create($values);
+    Role::load(Role::ANONYMOUS_ID)
+      ->grantPermission('edit any article content')
+      ->save();
+    $payload = Json::encode([
+      'data' => [
+        'type' => 'article',
+        'id' => $this->node->uuid(),
+        'attributes' => [
+          'title' => '',
+          'field_relationships' => '',
+        ],
+      ],
+    ]);
+    $request = new Request([], [], [], [], [], [], $payload);
+
+    // Create a new EntityResource that uses uuid.
+    $entity_resource = $this->buildEntityResource('node', 'article');
+    $response = $entity_resource->patchIndividual($this->node, $parsed_node, $request);
+
+    // As a side effect, the node will also be saved.
+    $this->assertInstanceOf(JsonApiDocumentTopLevel::class, $response->getResponseData());
+    $updated_node = $response->getResponseData()->getData();
+    $this->assertInstanceOf(Node::class, $updated_node);
+    $this->assertSame($values['title'], $this->node->getTitle());
+    $this->assertSame($values['field_relationships'], $this->node->get('field_relationships')->getValue());
+    $this->assertEquals(200, $response->getStatusCode());
+  }
+
+  /**
+   * Provides data for the testPatchIndividual.
+   *
+   * @return array
+   *   The input data for the test function.
+   */
+  public function patchIndividualProvider() {
+    return [
+      [
+        [
+          'type' => 'article',
+          'title' => 'PATCHED',
+          'field_relationships' => [['target_id' => 1]],
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * @covers ::patchIndividual
+   * @dataProvider patchIndividualConfigProvider
+   */
+  public function testPatchIndividualConfig($values) {
+    // List of fields to be ignored.
+    $ignored_fields = ['uuid', 'entityTypeId', 'type'];
+    $node_type = NodeType::create([
+      'type' => 'test',
+      'name' => 'Test Type',
+      'description' => '',
+    ]);
+    $node_type->save();
+
+    $parsed_node_type = NodeType::create($values);
+    Role::load(Role::ANONYMOUS_ID)
+      ->grantPermission('administer content types')
+      ->save();
+    Role::load(Role::ANONYMOUS_ID)
+      ->grantPermission('edit any article content')
+      ->save();
+    $payload = Json::encode([
+      'data' => [
+        'type' => 'node_type',
+        'id' => $node_type->uuid(),
+        'attributes' => $values,
+      ],
+    ]);
+    $request = new Request([], [], [], [], [], [], $payload);
+
+    $entity_resource = $this->buildEntityResource('node', 'article');
+    $response = $entity_resource->patchIndividual($node_type, $parsed_node_type, $request);
+
+    // As a side effect, the node will also be saved.
+    $this->assertInstanceOf(JsonApiDocumentTopLevel::class, $response->getResponseData());
+    $updated_node_type = $response->getResponseData()->getData();
+    $this->assertInstanceOf(NodeType::class, $updated_node_type);
+    // If the field is ignored then we should not see a difference.
+    foreach ($values as $field_name => $value) {
+      in_array($field_name, $ignored_fields) ?
+        $this->assertNotSame($value, $node_type->get($field_name)) :
+        $this->assertSame($value, $node_type->get($field_name));
+    }
+    $this->assertEquals(200, $response->getStatusCode());
+  }
+
+  /**
+   * Provides data for the testPatchIndividualConfig.
+   *
+   * @return array
+   *   The input data for the test function.
+   */
+  public function patchIndividualConfigProvider() {
+    return [
+      [['description' => 'PATCHED', 'status' => FALSE]],
+      [[]],
+    ];
+  }
+
+  /**
+   * @covers ::patchIndividual
+   * @dataProvider patchIndividualConfigFailedProvider
+   * @expectedException \Drupal\Core\Config\ConfigException
+   */
+  public function testPatchIndividualFailedConfig($values) {
+    $this->testPatchIndividualConfig($values);
+  }
+
+  /**
+   * Provides data for the testPatchIndividualFailedConfig.
+   *
+   * @return array
+   *   The input data for the test function.
+   */
+  public function patchIndividualConfigFailedProvider() {
+    return [
+      [['uuid' => 'PATCHED']],
+      [['type' => 'article', 'status' => FALSE]],
+    ];
+  }
+
+  /**
+   * @covers ::deleteIndividual
+   */
+  public function testDeleteIndividual() {
+    $node = Node::create([
+      'type' => 'article',
+      'title' => 'Lorem ipsum',
+    ]);
+    $nid = $node->id();
+    $node->save();
+    Role::load(Role::ANONYMOUS_ID)
+      ->grantPermission('delete own article content')
+      ->save();
+    $entity_resource = $this->buildEntityResource('node', 'article');
+    $response = $entity_resource->deleteIndividual($node, new Request());
+    // As a side effect, the node will also be deleted.
+    $count = $this->container->get('entity_type.manager')
+      ->getStorage('node')
+      ->getQuery()
+      ->condition('nid', $nid)
+      ->count()
+      ->execute();
+    $this->assertEquals(0, $count);
+    $this->assertNull($response->getResponseData());
+    $this->assertEquals(204, $response->getStatusCode());
+  }
+
+  /**
+   * @covers ::deleteIndividual
+   */
+  public function testDeleteIndividualConfig() {
+    $node_type = NodeType::create([
+      'type' => 'test',
+      'name' => 'Test Type',
+      'description' => 'Lorem ipsum',
+    ]);
+    $id = $node_type->id();
+    $node_type->save();
+    Role::load(Role::ANONYMOUS_ID)
+      ->grantPermission('administer content types')
+      ->save();
+    $entity_resource = $this->buildEntityResource('node', 'article');
+    $response = $entity_resource->deleteIndividual($node_type, new Request());
+    // As a side effect, the node will also be deleted.
+    $count = $this->container->get('entity_type.manager')
+      ->getStorage('node_type')
+      ->getQuery()
+      ->condition('type', $id)
+      ->count()
+      ->execute();
+    $this->assertEquals(0, $count);
+    $this->assertNull($response->getResponseData());
+    $this->assertEquals(204, $response->getStatusCode());
+  }
+
+  /**
+   * @covers ::createRelationship
+   */
+  public function testCreateRelationship() {
+    $parsed_field_list = $this->container
+      ->get('plugin.manager.field.field_type')
+      ->createFieldItemList($this->node, 'field_relationships', [
+        ['target_id' => $this->node->id()],
+      ]);
+    Role::load(Role::ANONYMOUS_ID)
+      ->grantPermission('edit any article content')
+      ->save();
+
+    $entity_resource = $this->buildEntityResource('node', 'article');
+    $response = $entity_resource->createRelationship($this->node, 'field_relationships', $parsed_field_list, new Request());
+
+    // As a side effect, the node will also be saved.
+    $this->assertNotEmpty($this->node->id());
+    $this->assertInstanceOf(JsonApiDocumentTopLevel::class, $response->getResponseData());
+    $field_list = $response->getResponseData()->getData();
+    $this->assertInstanceOf(EntityReferenceFieldItemListInterface::class, $field_list);
+    $this->assertSame('field_relationships', $field_list->getName());
+    $this->assertEquals([['target_id' => 1]], $field_list->getValue());
+    $this->assertEquals(201, $response->getStatusCode());
+  }
+
+  /**
+   * @covers ::patchRelationship
+   * @dataProvider patchRelationshipProvider
+   */
+  public function testPatchRelationship($relationships) {
+    $this->node->field_relationships->appendItem(['target_id' => $this->node->id()]);
+    $this->node->save();
+    $parsed_field_list = $this->container
+      ->get('plugin.manager.field.field_type')
+      ->createFieldItemList($this->node, 'field_relationships', $relationships);
+    Role::load(Role::ANONYMOUS_ID)
+      ->grantPermission('edit any article content')
+      ->save();
+
+    $entity_resource = $this->buildEntityResource('node', 'article');
+    $response = $entity_resource->patchRelationship($this->node, 'field_relationships', $parsed_field_list, new Request());
+
+    // As a side effect, the node will also be saved.
+    $this->assertNotEmpty($this->node->id());
+    $this->assertInstanceOf(JsonApiDocumentTopLevel::class, $response->getResponseData());
+    $field_list = $response->getResponseData()->getData();
+    $this->assertInstanceOf(EntityReferenceFieldItemListInterface::class, $field_list);
+    $this->assertSame('field_relationships', $field_list->getName());
+    $this->assertEquals($relationships, $field_list->getValue());
+    $this->assertEquals(200, $response->getStatusCode());
+  }
+
+  /**
+   * Provides data for the testPatchRelationship.
+   *
+   * @return array
+   *   The input data for the test function.
+   */
+  public function patchRelationshipProvider() {
+    return [
+      // Replace relationships.
+      [[['target_id' => 2], ['target_id' => 1]]],
+      // Remove relationships.
+      [[]],
+    ];
+  }
+
+  /**
+   * @covers ::deleteRelationship
+   * @dataProvider deleteRelationshipProvider
+   */
+  public function testDeleteRelationship($deleted_rels, $kept_rels) {
+    $this->node->field_relationships->appendItem(['target_id' => $this->node->id()]);
+    $this->node->field_relationships->appendItem(['target_id' => $this->node2->id()]);
+    $this->node->save();
+    $parsed_field_list = $this->container
+      ->get('plugin.manager.field.field_type')
+      ->createFieldItemList($this->node, 'field_relationships', $deleted_rels);
+    Role::load(Role::ANONYMOUS_ID)
+      ->grantPermission('edit any article content')
+      ->save();
+
+    $entity_resource = $this->buildEntityResource('node', 'article');
+    $response = $entity_resource->deleteRelationship($this->node, 'field_relationships', $parsed_field_list, new Request());
+
+    // As a side effect, the node will also be saved.
+    $this->assertInstanceOf(JsonApiDocumentTopLevel::class, $response->getResponseData());
+    $field_list = $response->getResponseData()->getData();
+    $this->assertInstanceOf(EntityReferenceFieldItemListInterface::class, $field_list);
+    $this->assertSame('field_relationships', $field_list->getName());
+    $this->assertEquals($kept_rels, $field_list->getValue());
+    $this->assertEquals(201, $response->getStatusCode());
+  }
+
+  /**
+   * Provides data for the testDeleteRelationship.
+   *
+   * @return array
+   *   The input data for the test function.
+   */
+  public function deleteRelationshipProvider() {
+    return [
+      // Remove one relationship.
+      [[['target_id' => 1]], [['target_id' => 2]]],
+      // Remove all relationships.
+      [[['target_id' => 2], ['target_id' => 1]], []],
+      // Remove no relationship.
+      [[], [['target_id' => 1], ['target_id' => 2]]],
+    ];
+  }
+
+  /**
+   * Instantiates a test EntityResource.
+   *
+   * @param string $entity_type_id
+   *   The entity type ID.
+   * @param string $bundle
+   *   The bundle.
+   *
+   * @return \Drupal\jsonapi\Controller\EntityResource
+   *   The resource.
+   */
+  protected function buildEntityResource($entity_type_id, $bundle) {
+    // The fake route.
+    $route = new Route(NULL, [], [
+      '_entity_type' => $entity_type_id,
+      '_bundle' => $bundle,
+    ]);
+    // The request.
+    $request = new Request([], [], ['_route_object' => $route]);
+    $request_stack = new RequestStack();
+    $request_stack->push($request);
+    // Get the entity resource.
+    $current_context = new CurrentContext(
+      $this->container->get('jsonapi.resource_type.repository'),
+      $request_stack,
+      new CurrentRouteMatch($request_stack)
+    );
+    $this->container->set('jsonapi.current_context', $current_context);
+
+    return new EntityResource(
+      new ResourceType($entity_type_id, $bundle, NULL),
+      $this->container->get('entity_type.manager'),
+      $this->container->get('jsonapi.query_builder'),
+      $this->container->get('entity_field.manager'),
+      $current_context,
+      $this->container->get('plugin.manager.field.field_type'),
+      $this->container->get('jsonapi.link_manager')
+    );
+  }
+
+}
diff --git a/drupal/modules/jsonapi/tests/src/Kernel/Controller/EntryPointTest.php b/drupal/modules/jsonapi/tests/src/Kernel/Controller/EntryPointTest.php
new file mode 100644
index 0000000..e538ad1
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Kernel/Controller/EntryPointTest.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Kernel\Controller;
+
+use Drupal\Core\Cache\CacheableJsonResponse;
+use Drupal\jsonapi\Controller\EntryPoint;
+use Drupal\Tests\jsonapi\Kernel\JsonapiKernelTestBase;
+
+/**
+ * @coversDefaultClass \Drupal\jsonapi\Controller\EntryPoint
+ * @group jsonapi
+ */
+class EntryPointTest extends JsonapiKernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'node',
+    'jsonapi',
+    'serialization',
+    'system',
+    'user',
+  ];
+
+  /**
+   * @covers ::index
+   */
+  public function testIndex() {
+    $controller = new EntryPoint(
+      \Drupal::service('jsonapi.resource_type.repository'),
+      \Drupal::service('renderer'),
+      new CacheableJsonResponse()
+    );
+    $processed_response = $controller->index();
+    $this->assertEquals(
+      ['url.site'],
+      $processed_response->getCacheableMetadata()->getCacheContexts()
+    );
+    $data = json_decode($processed_response->getContent(), TRUE);
+    $links = $data['links'];
+    $this->assertRegExp('/.*\/jsonapi/', $links['self']);
+    $this->assertRegExp('/.*\/jsonapi\/user\/user/', $links['user--user']);
+    $this->assertRegExp('/.*\/jsonapi\/node_type\/node_type/', $links['node_type--node_type']);
+  }
+
+}
diff --git a/drupal/modules/jsonapi/tests/src/Kernel/EntityToJsonApiTest.php b/drupal/modules/jsonapi/tests/src/Kernel/EntityToJsonApiTest.php
new file mode 100644
index 0000000..b08accb
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Kernel/EntityToJsonApiTest.php
@@ -0,0 +1,200 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Kernel;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\file\Entity\File;
+use Drupal\jsonapi\EntityToJsonApi;
+use Drupal\jsonapi\LinkManager\LinkManager;
+use Drupal\node\Entity\Node;
+use Drupal\node\Entity\NodeType;
+use Drupal\taxonomy\Entity\Term;
+use Drupal\taxonomy\Entity\Vocabulary;
+use Drupal\Tests\image\Kernel\ImageFieldCreationTrait;
+use Drupal\user\Entity\Role;
+use Drupal\user\Entity\User;
+use Drupal\user\RoleInterface;
+use Prophecy\Argument;
+
+/**
+ * @coversDefaultClass \Drupal\jsonapi\EntityToJsonApi
+ * @group jsonapi
+ * @group jsonapi_serializer
+ */
+class EntityToJsonApiTest extends JsonapiKernelTestBase {
+
+  use ImageFieldCreationTrait;
+
+  /**
+   * System under test.
+   *
+   * @var \Drupal\jsonapi\EntityToJsonApi
+   */
+  protected $sut;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'jsonapi',
+    'field',
+    'node',
+    'serialization',
+    'system',
+    'taxonomy',
+    'text',
+    'user',
+    'file',
+    'image',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    // Add the entity schemas.
+    $this->installEntitySchema('node');
+    $this->installEntitySchema('user');
+    $this->installEntitySchema('taxonomy_term');
+    $this->installEntitySchema('file');
+    // Add the additional table schemas.
+    $this->installSchema('system', ['sequences']);
+    $this->installSchema('node', ['node_access']);
+    $this->installSchema('user', ['users_data']);
+    $this->installSchema('file', ['file_usage']);
+    $this->nodeType = NodeType::create([
+      'type' => 'article',
+    ]);
+    $this->nodeType->save();
+    $this->createEntityReferenceField(
+      'node',
+      'article',
+      'field_tags',
+      'Tags',
+      'taxonomy_term',
+      'default',
+      ['target_bundles' => ['tags']],
+      FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
+    );
+
+    $this->createImageField('field_image', 'article');
+
+    $this->user = User::create([
+      'name' => 'user1',
+      'mail' => 'user@localhost',
+    ]);
+    $this->user2 = User::create([
+      'name' => 'user2',
+      'mail' => 'user2@localhost',
+    ]);
+
+    $this->user->save();
+    $this->user2->save();
+
+    $this->vocabulary = Vocabulary::create(['name' => 'Tags', 'vid' => 'tags']);
+    $this->vocabulary->save();
+
+    $this->term1 = Term::create([
+      'name' => 'term1',
+      'vid' => $this->vocabulary->id(),
+    ]);
+    $this->term2 = Term::create([
+      'name' => 'term2',
+      'vid' => $this->vocabulary->id(),
+    ]);
+
+    $this->term1->save();
+    $this->term2->save();
+
+    $this->file = File::create([
+      'uri' => 'public://example.png',
+      'filename' => 'example.png',
+    ]);
+    $this->file->save();
+
+    $this->node = Node::create([
+      'title' => 'dummy_title',
+      'type' => 'article',
+      'uid' => 1,
+      'field_tags' => [
+        ['target_id' => $this->term1->id()],
+        ['target_id' => $this->term2->id()],
+      ],
+      'field_image' => [
+        [
+          'target_id' => $this->file->id(),
+          'alt' => 'test alt',
+          'title' => 'test title',
+          'width' => 10,
+          'height' => 11,
+        ],
+      ],
+    ]);
+
+    $this->node->save();
+
+    $link_manager = $this->prophesize(LinkManager::class);
+    $link_manager
+      ->getEntityLink(Argument::any(), Argument::any(), Argument::type('array'), Argument::type('string'))
+      ->willReturn('dummy_entity_link');
+    $link_manager
+      ->getRequestLink(Argument::any())
+      ->willReturn('dummy_document_link');
+    $this->container->set('jsonapi.link_manager', $link_manager->reveal());
+
+    $this->nodeType = NodeType::load('article');
+
+    $this->role = Role::create([
+      'id' => RoleInterface::ANONYMOUS_ID,
+      'permissions' => [
+        'access content',
+      ],
+    ]);
+    $this->role->save();
+    $this->sut = \Drupal::service('jsonapi.entity.to_jsonapi');
+  }
+
+  /**
+   * @covers ::serialize
+   * @covers ::normalize
+   */
+  public function testSerialize() {
+    $entities = [
+      $this->node,
+      $this->user,
+      $this->file,
+      $this->term1,
+      // Make sure we also support configuration entities.
+      $this->vocabulary,
+      $this->nodeType,
+      $this->role,
+    ];
+    array_walk(
+      $entities,
+      function ($entity) {
+        $output = $this->sut->serialize($entity);
+        $this->assertInternalType('string', $output);
+        $this->assertJsonApi(Json::decode($output));
+        $output = $this->sut->normalize($entity);
+        $this->assertInternalType('array', $output);
+        $this->assertJsonApi($output);
+      }
+    );
+  }
+
+  /**
+   * Helper to assert if a string is valid JSON API.
+   *
+   * @param array $structured
+   *   The JSON API data to check.
+   */
+  protected function assertJsonApi(array $structured) {
+    $this->assertNotEmpty($structured['data']['type']);
+    $this->assertNotEmpty($structured['data']['id']);
+    $this->assertNotEmpty($structured['data']['attributes']);
+    $this->assertInternalType('string', $structured['links']['self']);
+  }
+
+}
diff --git a/drupal/modules/jsonapi/tests/src/Kernel/Field/FileDownloadUrlTest.php b/drupal/modules/jsonapi/tests/src/Kernel/Field/FileDownloadUrlTest.php
new file mode 100644
index 0000000..30d50d4
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Kernel/Field/FileDownloadUrlTest.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Kernel\Field;
+
+use Drupal\file\Entity\File;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * @coversDefaultClass \Drupal\jsonapi\Field\FileDownloadUrl
+ * @group jsonapi
+ */
+class FileDownloadUrlTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'jsonapi',
+    'file',
+    'serialization',
+    'user',
+  ];
+
+  /**
+   * @var \Drupal\file\Entity\File
+   */
+  protected $file;
+
+  /**
+   * @var string
+   *   The test filename.
+   */
+  protected $filename = 'druplicon.txt';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->installEntitySchema('file');
+    $this->installSchema('file', ['file_usage']);
+
+    // Create a new file entity.
+    $this->file = File::create([
+      'filename' => $this->filename,
+      'uri' => sprintf('public://%s', $this->filename),
+      'filemime' => 'text/plain',
+      'status' => FILE_STATUS_PERMANENT,
+    ]);
+
+    $this->file->save();
+  }
+
+  /**
+   * Test the URL computed field.
+   */
+  public function testUrlField() {
+    $url_field = $this->file->get('url');
+    // Test all the different ways to access a field item.
+    $values = [
+      $url_field->value,
+      $url_field->getValue()[0]['value'],
+      $url_field->get(0)->toArray()['value'],
+      $url_field->first()->getValue()['value'],
+    ];
+    array_walk($values, function ($value) {
+      $this->assertContains('simpletest', $value);
+      $this->assertContains($this->filename, $value);
+    });
+  }
+
+}
diff --git a/drupal/modules/jsonapi/tests/src/Kernel/JsonapiKernelTestBase.php b/drupal/modules/jsonapi/tests/src/Kernel/JsonapiKernelTestBase.php
new file mode 100644
index 0000000..fd39cc1
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Kernel/JsonapiKernelTestBase.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Kernel;
+
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * @internal
+ */
+abstract class JsonapiKernelTestBase extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['jsonapi'];
+
+  /**
+   * Creates a field of an entity reference field storage on the bundle.
+   *
+   * @param string $entity_type
+   *   The type of entity the field will be attached to.
+   * @param string $bundle
+   *   The bundle name of the entity the field will be attached to.
+   * @param string $field_name
+   *   The name of the field; if it exists, a new instance of the existing.
+   *   field will be created.
+   * @param string $field_label
+   *   The label of the field.
+   * @param string $target_entity_type
+   *   The type of the referenced entity.
+   * @param string $selection_handler
+   *   The selection handler used by this field.
+   * @param array $handler_settings
+   *   An array of settings supported by the selection handler specified above.
+   *   (e.g. 'target_bundles', 'sort', 'auto_create', etc).
+   * @param int $cardinality
+   *   The cardinality of the field.
+   *
+   * @see \Drupal\Core\Entity\Plugin\EntityReferenceSelection\SelectionBase::buildConfigurationForm()
+   */
+  protected function createEntityReferenceField($entity_type, $bundle, $field_name, $field_label, $target_entity_type, $selection_handler = 'default', $handler_settings = [], $cardinality = 1) {
+    // Look for or add the specified field to the requested entity bundle.
+    if (!FieldStorageConfig::loadByName($entity_type, $field_name)) {
+      FieldStorageConfig::create([
+        'field_name' => $field_name,
+        'type' => 'entity_reference',
+        'entity_type' => $entity_type,
+        'cardinality' => $cardinality,
+        'settings' => [
+          'target_type' => $target_entity_type,
+        ],
+      ])->save();
+    }
+    if (!FieldConfig::loadByName($entity_type, $bundle, $field_name)) {
+      FieldConfig::create([
+        'field_name' => $field_name,
+        'entity_type' => $entity_type,
+        'bundle' => $bundle,
+        'label' => $field_label,
+        'settings' => [
+          'handler' => $selection_handler,
+          'handler_settings' => $handler_settings,
+        ],
+      ])->save();
+    }
+  }
+
+}
diff --git a/drupal/modules/jsonapi/tests/src/Kernel/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php b/drupal/modules/jsonapi/tests/src/Kernel/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php
new file mode 100644
index 0000000..2cd2220
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Kernel/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php
@@ -0,0 +1,703 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Kernel\Normalizer;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\file\Entity\File;
+use Drupal\jsonapi\ResourceType\ResourceType;
+use Drupal\jsonapi\LinkManager\LinkManager;
+use Drupal\jsonapi\Normalizer\JsonApiDocumentTopLevelNormalizer;
+use Drupal\jsonapi\Resource\JsonApiDocumentTopLevel;
+use Drupal\node\Entity\Node;
+use Drupal\node\Entity\NodeType;
+use Drupal\jsonapi\ResourceResponse;
+use Drupal\taxonomy\Entity\Term;
+use Drupal\taxonomy\Entity\Vocabulary;
+use Drupal\Tests\image\Kernel\ImageFieldCreationTrait;
+use Drupal\Tests\jsonapi\Kernel\JsonapiKernelTestBase;
+use Drupal\user\Entity\Role;
+use Drupal\user\Entity\User;
+use Drupal\user\RoleInterface;
+use Prophecy\Argument;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\HttpFoundation\ParameterBag;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+use Symfony\Component\Routing\Route;
+
+/**
+ * @coversDefaultClass \Drupal\jsonapi\Normalizer\JsonApiDocumentTopLevelNormalizer
+ * @group jsonapi
+ */
+class JsonApiDocumentTopLevelNormalizerTest extends JsonapiKernelTestBase {
+
+  use ImageFieldCreationTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'jsonapi',
+    'field',
+    'node',
+    'serialization',
+    'system',
+    'taxonomy',
+    'text',
+    'user',
+    'file',
+    'image',
+  ];
+
+  /**
+   * A node to normalize.
+   *
+   * @var \Drupal\Core\Entity\EntityInterface
+   */
+  protected $node;
+
+  /**
+   * A user to normalize.
+   *
+   * @var \Drupal\user\Entity\User
+   */
+  protected $user;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    // Add the entity schemas.
+    $this->installEntitySchema('node');
+    $this->installEntitySchema('user');
+    $this->installEntitySchema('taxonomy_term');
+    $this->installEntitySchema('file');
+    // Add the additional table schemas.
+    $this->installSchema('system', ['sequences']);
+    $this->installSchema('node', ['node_access']);
+    $this->installSchema('user', ['users_data']);
+    $this->installSchema('file', ['file_usage']);
+    $type = NodeType::create([
+      'type' => 'article',
+    ]);
+    $type->save();
+    $this->createEntityReferenceField(
+      'node',
+      'article',
+      'field_tags',
+      'Tags',
+      'taxonomy_term',
+      'default',
+      ['target_bundles' => ['tags']],
+      FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
+    );
+
+    $this->createImageField('field_image', 'article');
+
+    $this->user = User::create([
+      'name' => 'user1',
+      'mail' => 'user@localhost',
+    ]);
+    $this->user2 = User::create([
+      'name' => 'user2',
+      'mail' => 'user2@localhost',
+    ]);
+
+    $this->user->save();
+    $this->user2->save();
+
+    $this->vocabulary = Vocabulary::create(['name' => 'Tags', 'vid' => 'tags']);
+    $this->vocabulary->save();
+
+    $this->term1 = Term::create([
+      'name' => 'term1',
+      'vid' => $this->vocabulary->id(),
+    ]);
+    $this->term2 = Term::create([
+      'name' => 'term2',
+      'vid' => $this->vocabulary->id(),
+    ]);
+
+    $this->term1->save();
+    $this->term2->save();
+
+    $this->file = File::create([
+      'uri' => 'public://example.png',
+      'filename' => 'example.png',
+    ]);
+    $this->file->save();
+
+    $this->node = Node::create([
+      'title' => 'dummy_title',
+      'type' => 'article',
+      'uid' => 1,
+      'field_tags' => [
+        ['target_id' => $this->term1->id()],
+        ['target_id' => $this->term2->id()],
+      ],
+      'field_image' => [
+        [
+          'target_id' => $this->file->id(),
+          'alt' => 'test alt',
+          'title' => 'test title',
+          'width' => 10,
+          'height' => 11,
+        ],
+      ],
+    ]);
+
+    $this->node->save();
+
+    $link_manager = $this->prophesize(LinkManager::class);
+    $link_manager
+      ->getEntityLink(Argument::any(), Argument::any(), Argument::type('array'), Argument::type('string'))
+      ->willReturn('dummy_entity_link');
+    $link_manager
+      ->getRequestLink(Argument::any())
+      ->willReturn('dummy_document_link');
+    $this->container->set('jsonapi.link_manager', $link_manager->reveal());
+
+    $this->nodeType = NodeType::load('article');
+
+    Role::create([
+      'id' => RoleInterface::ANONYMOUS_ID,
+      'permissions' => [
+        'access content',
+      ],
+    ])->save();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function tearDown() {
+    if ($this->node) {
+      $this->node->delete();
+    }
+    if ($this->term1) {
+      $this->term1->delete();
+    }
+    if ($this->term2) {
+      $this->term2->delete();
+    }
+    if ($this->vocabulary) {
+      $this->vocabulary->delete();
+    }
+    if ($this->user) {
+      $this->user->delete();
+    }
+    if ($this->user2) {
+      $this->user2->delete();
+    }
+  }
+
+  /**
+   * @covers ::normalize
+   */
+  public function testNormalize() {
+    list($request, $resource_type) = $this->generateProphecies('node', 'article');
+    $request->query = new ParameterBag([
+      'fields' => [
+        'node--article' => 'title,type,uid,field_tags,field_image',
+        'user--user' => 'name',
+      ],
+      'include' => 'uid,field_tags,field_image',
+    ]);
+
+    $response = new ResourceResponse();
+    $normalized = $this
+      ->container
+      ->get('serializer.normalizer.jsonapi_document_toplevel.jsonapi')
+      ->normalize(
+        new JsonApiDocumentTopLevel($this->node),
+        'api_json',
+        [
+          'request' => $request,
+          'resource_type' => $resource_type,
+          'cacheable_metadata' => $response->getCacheableMetadata(),
+        ]
+      );
+    $this->assertSame($normalized['data']['attributes']['title'], 'dummy_title');
+    $this->assertEquals($normalized['data']['id'], $this->node->uuid());
+    $this->assertSame([
+      'data' => [
+        'type' => 'node_type--node_type',
+        'id' => NodeType::load('article')->uuid(),
+      ],
+      'links' => [
+        'self' => 'dummy_entity_link',
+        'related' => 'dummy_entity_link',
+      ],
+    ], $normalized['data']['relationships']['type']);
+    $this->assertTrue(!isset($normalized['data']['attributes']['created']));
+    $this->assertEquals([
+      'alt' => 'test alt',
+      'title' => 'test title',
+      'width' => 10,
+      'height' => 11,
+    ], $normalized['data']['relationships']['field_image']['data']['meta']);
+    $this->assertSame('node--article', $normalized['data']['type']);
+    $this->assertEquals([
+      'data' => [
+        'type' => 'user--user',
+        'id' => $this->user->uuid(),
+      ],
+      'links' => [
+        'self' => 'dummy_entity_link',
+        'related' => 'dummy_entity_link',
+      ],
+    ], $normalized['data']['relationships']['uid']);
+    $this->assertEquals(
+      "The current user is not allowed to GET the selected resource. The 'access user profiles' permission is required and the user must be active.",
+      $normalized['meta']['errors'][0]['detail']
+    );
+    $this->assertEquals(403, $normalized['meta']['errors'][0]['status']);
+    $this->assertEquals($this->term1->uuid(), $normalized['included'][0]['id']);
+    $this->assertEquals('taxonomy_term--tags', $normalized['included'][0]['type']);
+    $this->assertEquals($this->term1->label(), $normalized['included'][0]['attributes']['name']);
+    $this->assertTrue(!isset($normalized['included'][0]['attributes']['created']));
+    // Make sure that the cache tags for the includes and the requested entities
+    // are bubbling as expected.
+    $this->assertSame(
+      ['file:1', 'node:1', 'taxonomy_term:1', 'taxonomy_term:2'],
+      $response->getCacheableMetadata()->getCacheTags()
+    );
+    $this->assertSame(
+      Cache::PERMANENT,
+      $response->getCacheableMetadata()->getCacheMaxAge()
+    );
+  }
+
+  /**
+   * @covers ::normalize
+   */
+  public function testNormalizeRelated() {
+    list($request, $resource_type) = $this->generateProphecies('node', 'article', 'uid');
+    $request->query = new ParameterBag([
+      'fields' => [
+        'user--user' => 'name,roles',
+      ],
+      'include' => 'roles',
+    ]);
+    $document_wrapper = $this->prophesize(JsonApiDocumentTopLevel::class);
+    $author = $this->node->get('uid')->entity;
+    $document_wrapper->getData()->willReturn($author);
+
+    $response = new ResourceResponse();
+    $normalized = $this
+      ->container
+      ->get('serializer.normalizer.jsonapi_document_toplevel.jsonapi')
+      ->normalize(
+        $document_wrapper->reveal(),
+        'api_json',
+        [
+          'request' => $request,
+          'resource_type' => $resource_type,
+          'cacheable_metadata' => $response->getCacheableMetadata(),
+        ]
+      );
+    $this->assertSame($normalized['data']['attributes']['name'], 'user1');
+    $this->assertEquals($normalized['data']['id'], User::load(1)->uuid());
+    $this->assertEquals($normalized['data']['type'], 'user--user');
+    // Make sure that the cache tags for the includes and the requested entities
+    // are bubbling as expected.
+    $this->assertSame(['user:1'], $response->getCacheableMetadata()
+      ->getCacheTags());
+    $this->assertSame(Cache::PERMANENT, $response->getCacheableMetadata()
+      ->getCacheMaxAge());
+  }
+
+  /**
+   * @covers ::normalize
+   */
+  public function testNormalizeUuid() {
+    list($request, $resource_type) = $this->generateProphecies('node', 'article', 'uuid');
+    $document_wrapper = $this->prophesize(JsonApiDocumentTopLevel::class);
+    $document_wrapper->getData()->willReturn($this->node);
+    $request->query = new ParameterBag([
+      'fields' => [
+        'node--article' => 'title,type,uid,field_tags',
+        'user--user' => 'name',
+      ],
+      'include' => 'uid,field_tags',
+    ]);
+
+    $response = new ResourceResponse();
+    $normalized = $this
+      ->container
+      ->get('serializer.normalizer.jsonapi_document_toplevel.jsonapi')
+      ->normalize(
+        $document_wrapper->reveal(),
+        'api_json',
+        [
+          'request' => $request,
+          'resource_type' => $resource_type,
+          'cacheable_metadata' => $response->getCacheableMetadata(),
+        ]
+      );
+    $this->assertStringMatchesFormat($this->node->uuid(), $normalized['data']['id']);
+    $this->assertEquals($this->node->type->entity->uuid(), $normalized['data']['relationships']['type']['data']['id']);
+    $this->assertEquals($this->user->uuid(), $normalized['data']['relationships']['uid']['data']['id']);
+    $this->assertFalse(empty($normalized['included'][0]['id']));
+    $this->assertFalse(empty($normalized['meta']['errors']));
+    $this->assertEquals($this->term1->uuid(), $normalized['included'][0]['id']);
+    // Make sure that the cache tags for the includes and the requested entities
+    // are bubbling as expected.
+    $this->assertSame(
+      ['node:1', 'taxonomy_term:1', 'taxonomy_term:2'],
+      $response->getCacheableMetadata()->getCacheTags()
+    );
+  }
+
+  /**
+   * @covers ::normalize
+   */
+  public function testNormalizeException() {
+    list($request, $resource_type) = $this->generateProphecies('node', 'article', 'id');
+    $document_wrapper = $this->prophesize(JsonApiDocumentTopLevel::class);
+    $document_wrapper->getData()->willReturn($this->node);
+    $request->query = new ParameterBag([
+      'fields' => [
+        'node--article' => 'title,type,uid',
+        'user--user' => 'name',
+      ],
+      'include' => 'uid',
+    ]);
+
+    $response = new ResourceResponse();
+    $normalized = $this
+      ->container
+      ->get('serializer')
+      ->serialize(
+        new BadRequestHttpException('Lorem'),
+        'api_json',
+        [
+          'request' => $request,
+          'resource_type' => $resource_type,
+          'cacheable_metadata' => $response->getCacheableMetadata(),
+          'data_wrapper' => 'errors',
+        ]
+      );
+    $normalized = Json::decode($normalized);
+    $this->assertNotEmpty($normalized['errors']);
+    $this->assertArrayNotHasKey('data', $normalized);
+    $this->assertEquals(400, $normalized['errors'][0]['status']);
+    $this->assertEquals('Lorem', $normalized['errors'][0]['detail']);
+    $this->assertEquals(['info' => 'http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.1'], $normalized['errors'][0]['links']);
+  }
+
+  /**
+   * @covers ::normalize
+   */
+  public function testNormalizeConfig() {
+    list($request, $resource_type) = $this->generateProphecies('node_type', 'node_type', 'id');
+    $document_wrapper = $this->prophesize(JsonApiDocumentTopLevel::class);
+    $document_wrapper->getData()->willReturn($this->nodeType);
+    $request->query = new ParameterBag([
+      'fields' => [
+        'node_type--node_type' => 'uuid,display_submitted',
+      ],
+      'include' => NULL,
+    ]);
+
+    $response = new ResourceResponse();
+    $normalized = $this
+      ->container
+      ->get('serializer.normalizer.jsonapi_document_toplevel.jsonapi')
+      ->normalize($document_wrapper->reveal(), 'api_json', [
+        'request' => $request,
+        'resource_type' => $resource_type,
+        'cacheable_metadata' => $response->getCacheableMetadata(),
+      ]);
+    $this->assertTrue(empty($normalized['data']['attributes']['type']));
+    $this->assertTrue(!empty($normalized['data']['attributes']['uuid']));
+    $this->assertSame($normalized['data']['attributes']['display_submitted'], TRUE);
+    $this->assertSame($normalized['data']['id'], NodeType::load('article')->uuid());
+    $this->assertSame($normalized['data']['type'], 'node_type--node_type');
+    // Make sure that the cache tags for the includes and the requested entities
+    // are bubbling as expected.
+    $this->assertSame(['config:node.type.article'], $response->getCacheableMetadata()
+      ->getCacheTags());
+  }
+
+  /**
+   * Try to POST a node and check if it exists afterwards.
+   *
+   * @covers ::denormalize
+   */
+  public function testDenormalize() {
+    $payload = '{"type":"article", "data":{"attributes":{"title":"Testing article"}}}';
+
+    list($request, $resource_type) = $this->generateProphecies('node', 'article', 'id');
+    $node = $this
+      ->container
+      ->get('serializer.normalizer.jsonapi_document_toplevel.jsonapi')
+      ->denormalize(Json::decode($payload), JsonApiDocumentTopLevelNormalizer::class, 'api_json', [
+        'request' => $request,
+        'resource_type' => $resource_type,
+      ]);
+    $this->assertInstanceOf('\Drupal\node\Entity\Node', $node);
+    $this->assertSame('Testing article', $node->getTitle());
+  }
+
+  /**
+   * Try to POST a node and check if it exists afterwards.
+   *
+   * @covers ::denormalize
+   */
+  public function testDenormalizeUuid() {
+    $configurations = [
+      // Good data.
+      [
+        [
+          [$this->term2->uuid(), $this->term1->uuid()],
+          $this->user2->uuid(),
+        ],
+        [
+          [$this->term2->id(), $this->term1->id()],
+          $this->user2->id(),
+        ],
+      ],
+      // Good data, without any tags.
+      [
+        [
+          [],
+          $this->user2->uuid(),
+        ],
+        [
+          [],
+          $this->user2->id(),
+        ],
+      ],
+      // Bad data in first tag.
+      [
+        [
+          ['invalid-uuid', $this->term1->uuid()],
+          $this->user2->uuid(),
+        ],
+        [
+          [$this->term1->id()],
+          $this->user2->id(),
+        ],
+      ],
+      // Bad data in user and first tag.
+      [
+        [
+          ['invalid-uuid', $this->term1->uuid()],
+          'also-invalid-uuid',
+        ],
+        [
+          [$this->term1->id()],
+          NULL,
+        ],
+      ],
+    ];
+
+    foreach ($configurations as $configuration) {
+      list($payload_data, $expected) = $this->denormalizeUuidProviderBuilder($configuration);
+      $payload = Json::encode($payload_data);
+
+      list($request, $resource_type) = $this->generateProphecies('node', 'article');
+      $this->container->get('request_stack')->push($request);
+      $node = $this
+        ->container
+        ->get('serializer.normalizer.jsonapi_document_toplevel.jsonapi')
+        ->denormalize(Json::decode($payload), JsonApiDocumentTopLevelNormalizer::class, 'api_json', [
+          'request' => $request,
+          'resource_type' => $resource_type,
+        ]);
+
+      /* @var \Drupal\node\Entity\Node $node */
+      $this->assertInstanceOf('\Drupal\node\Entity\Node', $node);
+      $this->assertSame('Testing article', $node->getTitle());
+      if (!empty($expected['user_id'])) {
+        $owner = $node->getOwner();
+        $this->assertEquals($expected['user_id'], $owner->id());
+      }
+      $tags = $node->get('field_tags')->getValue();
+      if (!empty($expected['tag_ids'][0])) {
+        $this->assertEquals($expected['tag_ids'][0], $tags[0]['target_id']);
+      }
+      else {
+        $this->assertArrayNotHasKey(0, $tags);
+      }
+      if (!empty($expected['tag_ids'][1])) {
+        $this->assertEquals($expected['tag_ids'][1], $tags[1]['target_id']);
+      }
+      else {
+        $this->assertArrayNotHasKey(1, $tags);
+      }
+    }
+  }
+
+  /**
+   * Try to POST a node with related resource of invalid type, as well as one
+   * with no type.
+   */
+  public function testDenormalizeInvalidTypeAndNoType() {
+    $payload_data = [
+      'type' => 'node--article',
+      'data' => [
+        'attributes' => [
+          'title' => 'Testing article',
+          'id' => '33095485-70D2-4E51-A309-535CC5BC0115',
+        ],
+        'relationships' => [
+          'uid' => [
+            'data' => [
+              'type' => 'user--user',
+              'id' => $this->user2->uuid(),
+            ],
+          ],
+          'field_tags' => [
+            'data' => [
+              [
+                'type' => 'foobar',
+                'id' => $this->term1->uuid(),
+              ],
+            ],
+          ],
+        ],
+      ],
+    ];
+
+    // Test relationship member with invalid type.
+    $payload = Json::encode($payload_data);
+    list($request, $resource_type) = $this->generateProphecies('node', 'article');
+    $this->container->get('request_stack')->push($request);
+    try {
+      $this->container->get('serializer.normalizer.jsonapi_document_toplevel.jsonapi')
+        ->denormalize(Json::decode($payload), JsonApiDocumentTopLevelNormalizer::class, 'api_json', [
+          'request' => $request,
+          'resource_type' => $resource_type,
+        ]);
+
+      $this->fail('No assertion thrown for invalid type');
+    }
+    catch (BadRequestHttpException $e) {
+      $this->assertEquals("Invalid type specified for related resource: 'foobar'", $e->getMessage());
+    }
+
+    // Test relationship member with no type.
+    unset($payload_data['data']['relationships']['field_tags']['data'][0]['type']);
+
+    $payload = Json::encode($payload_data);
+    list($request, $resource_type) = $this->generateProphecies('node', 'article');
+    $this->container->get('request_stack')->push($request);
+    try {
+      $this->container->get('serializer.normalizer.jsonapi_document_toplevel.jsonapi')
+        ->denormalize(Json::decode($payload), JsonApiDocumentTopLevelNormalizer::class, 'api_json', [
+          'request' => $request,
+          'resource_type' => $resource_type,
+        ]);
+
+      $this->fail('No assertion thrown for missing type');
+    }
+    catch (BadRequestHttpException $e) {
+      $this->assertEquals("No type specified for related resource", $e->getMessage());
+    }
+  }
+
+  /**
+   * We cannot use a PHPUnit data provider because our data depends on $this.
+   *
+   * @param array $options
+   *
+   * @return array
+   *   The test data.
+   */
+  protected function denormalizeUuidProviderBuilder($options) {
+    list($input, $expected) = $options;
+    list($input_tag_uuids, $input_user_uuid) = $input;
+    list($expected_tag_ids, $expected_user_id) = $expected;
+
+    $node = [
+      [
+        'type' => 'node--article',
+        'data' => [
+          'attributes' => [
+            'title' => 'Testing article',
+            'id' => '33095485-70D2-4E51-A309-535CC5BC0115',
+          ],
+          'relationships' => [
+            'uid' => [
+              'data' => [
+                'type' => 'user--user',
+                'id' => $input_user_uuid,
+              ],
+            ],
+            'field_tags' => [
+              'data' => [],
+            ],
+          ],
+        ],
+      ],
+      [
+        'tag_ids' => $expected_tag_ids,
+        'user_id' => $expected_user_id,
+      ],
+    ];
+
+    if (isset($input_tag_uuids[0])) {
+      $node[0]['data']['relationships']['field_tags']['data'][0] = [
+        'type' => 'taxonomy_term--tags',
+        'id' => $input_tag_uuids[0],
+      ];
+    }
+    if (isset($input_tag_uuids[1])) {
+      $node[0]['data']['relationships']['field_tags']['data'][1] = [
+        'type' => 'taxonomy_term--tags',
+        'id' => $input_tag_uuids[1],
+      ];
+    }
+    return $node;
+  }
+
+  /**
+   * Generates the prophecies for the mocked entity request.
+   *
+   * @param string $entity_type_id
+   *   The ID of the entity type. Ex: node.
+   * @param string $bundle
+   *   The bundle. Ex: article.
+   *
+   * @return array
+   *   A numeric array containing the request and the ResourceType.
+   */
+  protected function generateProphecies($entity_type_id, $bundle, $related_property = NULL) {
+    $path = sprintf('/%s/%s', $entity_type_id, $bundle);
+    $path = $related_property ?
+      sprintf('%s/%s', $path, $related_property) :
+      $path;
+
+    $route = new Route($path, [
+      '_on_relationship' => NULL,
+    ], [
+      '_entity_type' => $entity_type_id,
+      '_bundle' => $bundle,
+    ]);
+    $request = new Request([], [], [
+      RouteObjectInterface::ROUTE_OBJECT => $route,
+    ]);
+    /* @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
+    $entity_type_manager = $this->container->get('entity_type.manager');
+
+    $resource_type = new ResourceType(
+      $entity_type_id,
+      $bundle,
+      $entity_type_manager->getDefinition($entity_type_id)->getClass()
+    );
+
+    /* @var \Symfony\Component\HttpFoundation\RequestStack $request_stack */
+    $request_stack = $this->container->get('request_stack');
+    $request_stack->push($request);
+    $this->container->set('request_stack', $request_stack);
+    $this->container->get('serializer');
+
+    return [$request, $resource_type];
+  }
+
+}
diff --git a/drupal/modules/jsonapi/tests/src/Kernel/ResourceType/ResourceTypeRepositoryTest.php b/drupal/modules/jsonapi/tests/src/Kernel/ResourceType/ResourceTypeRepositoryTest.php
new file mode 100644
index 0000000..f2cd644
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Kernel/ResourceType/ResourceTypeRepositoryTest.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Kernel\ResourceType;
+
+use Drupal\jsonapi\ResourceType\ResourceType;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\node\Entity\NodeType;
+
+/**
+ * @coversDefaultClass \Drupal\jsonapi\ResourceType\ResourceTypeRepository
+ * @group jsonapi
+ */
+class ResourceTypeRepositoryTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'node',
+    'jsonapi',
+    'serialization',
+    'system',
+    'user',
+  ];
+
+  /**
+   * The JSON API resource type repository under test.
+   *
+   * @var \Drupal\jsonapi\ResourceType\ResourceTypeRepository
+   */
+  protected $resourceTypeRepository;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    // Add the entity schemas.
+    $this->installEntitySchema('node');
+    $this->installEntitySchema('user');
+    // Add the additional table schemas.
+    $this->installSchema('system', ['sequences']);
+    $this->installSchema('node', ['node_access']);
+    $this->installSchema('user', ['users_data']);
+    NodeType::create([
+      'type' => 'article',
+    ])->save();
+    NodeType::create([
+      'type' => 'page',
+    ])->save();
+
+    $this->resourceTypeRepository = $this->container->get('jsonapi.resource_type.repository');
+  }
+
+  /**
+   * @covers ::all
+   */
+  public function testAll() {
+    // Make sure that there are resources being created.
+    $all = $this->resourceTypeRepository->all();
+    $this->assertNotEmpty($all);
+    array_walk($all, function (ResourceType $resource_type) {
+      $this->assertNotEmpty($resource_type->getDeserializationTargetClass());
+      $this->assertNotEmpty($resource_type->getEntityTypeId());
+      $this->assertNotEmpty($resource_type->getTypeName());
+    });
+  }
+
+  /**
+   * @covers ::get
+   * @dataProvider getProvider
+   */
+  public function testGet($entity_type_id, $bundle, $entity_class) {
+    // Make sure that there are resources being created.
+    $resource_type = $this->resourceTypeRepository->get($entity_type_id, $bundle);
+    $this->assertInstanceOf(ResourceType::class, $resource_type);
+    $this->assertSame($entity_class, $resource_type->getDeserializationTargetClass());
+    $this->assertSame($entity_type_id, $resource_type->getEntityTypeId());
+    $this->assertSame($bundle, $resource_type->getBundle());
+    $this->assertSame($entity_type_id . '--' . $bundle, $resource_type->getTypeName());
+  }
+
+  /**
+   * Data provider for testGet.
+   *
+   * @returns array
+   *   The data for the test method.
+   */
+  public function getProvider() {
+    return [
+      ['node', 'article', 'Drupal\node\Entity\Node'],
+      ['node_type', 'node_type', 'Drupal\node\Entity\NodeType'],
+      ['menu', 'menu', 'Drupal\system\Entity\Menu'],
+    ];
+  }
+
+}
diff --git a/drupal/modules/jsonapi/tests/src/Unit/Access/CustomQueryParameterNamesAccessCheckTest.php b/drupal/modules/jsonapi/tests/src/Unit/Access/CustomQueryParameterNamesAccessCheckTest.php
new file mode 100644
index 0000000..16295fc
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Unit/Access/CustomQueryParameterNamesAccessCheckTest.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Unit\Access;
+
+use Drupal\jsonapi\Access\CustomQueryParameterNamesAccessCheck;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * @coversDefaultClass \Drupal\jsonapi\Access\CustomQueryParameterNamesAccessCheck
+ * @group jsonapi
+ */
+class CustomQueryParameterNamesAccessCheckTest extends \PHPUnit_Framework_TestCase {
+
+  /**
+   * @dataProvider providerTestAccess
+   * @covers ::access
+   * @covers ::validate
+   */
+  public function testAccess($name, $valid) {
+    $access_checker = new CustomQueryParameterNamesAccessCheck();
+
+    $request = new Request();
+    $request->attributes->set('_json_api_params', [$name => '123']);
+    $result = $access_checker->access($request);
+
+    if ($valid) {
+      $this->assertTrue($result->isAllowed());
+    }
+    else {
+      $this->assertFalse($result->isAllowed());
+    }
+  }
+
+  public function providerTestAccess() {
+    $data = [];
+
+    $data['Official query parameter: sort'] = ['sort', TRUE];
+    $data['Official query parameter: page'] = ['page', TRUE];
+    $data['Official query parameter: filter'] = ['filter', TRUE];
+
+    $data['Valid member, but invalid custom query parameter'] = ['foobar', FALSE];
+
+    $data['Valid custom query parameter: dash'] = ['foo-bar', TRUE];
+    $data['Valid custom query parameter: underscore'] = ['foo_bar', TRUE];
+    $data['Valid custom query parameter: camelcase'] = ['fooBar', TRUE];
+
+    return $data;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/tests/src/Unit/Context/CurrentContextTest.php b/drupal/modules/jsonapi/tests/src/Unit/Context/CurrentContextTest.php
new file mode 100644
index 0000000..b0f465d
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Unit/Context/CurrentContextTest.php
@@ -0,0 +1,155 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Unit\Context;
+
+use Drupal\Core\Routing\CurrentRouteMatch;
+use Drupal\jsonapi\Context\CurrentContext;
+use Drupal\jsonapi\ResourceType\ResourceType;
+use Drupal\jsonapi\ResourceType\ResourceTypeRepository;
+use Drupal\jsonapi\Routing\Param\Filter;
+use Drupal\jsonapi\Routing\Param\Sort;
+use Drupal\jsonapi\Routing\Param\OffsetPage;
+use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\node\NodeInterface;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Route;
+
+/**
+ * @coversDefaultClass \Drupal\jsonapi\Context\CurrentContext
+ * @group jsonapi
+ */
+class CurrentContextTest extends UnitTestCase {
+
+  /**
+   * A mock for the current route.
+   *
+   * @var \Symfony\Component\Routing\Route
+   */
+  protected $currentRoute;
+
+  /**
+   * A mock for the JSON API resource type repository.
+   *
+   * @var \Drupal\jsonapi\ResourceType\ResourceTypeRepository
+   */
+  protected $resourceTypeRepository;
+
+  /**
+   * A mock for the entity field manager.
+   *
+   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
+   */
+  protected $fieldManager;
+
+  /**
+   * A request stack.
+   *
+   * @var \Symfony\Component\HttpFoundation\RequestStack
+   */
+  protected $requestStack;
+
+  /**
+   * @var \Drupal\Core\Routing\StackedRouteMatchInterface
+   */
+  protected $routeMatcher;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    // Create a mock for the entity field manager.
+    $this->fieldManager = $this->prophesize(EntityFieldManagerInterface::CLASS)->reveal();
+
+    // Create a mock for the current route match.
+    $this->currentRoute = new Route(
+      '/jsonapi/articles',
+      [],
+      ['_entity_type' => 'node', '_bundle' => 'article']
+    );
+
+    // Create a mock for the ResourceTypeRepository service.
+    $resource_type_repository_prophecy = $this->prophesize(ResourceTypeRepository::CLASS);
+    $resource_type_repository_prophecy->get('node', 'article')
+      ->willReturn(new ResourceType('node', 'article', NodeInterface::class));
+    $this->resourceTypeRepository = $resource_type_repository_prophecy->reveal();
+
+    $this->requestStack = new RequestStack();
+    $this->requestStack->push(new Request([], [], [
+      '_json_api_params' => [
+        'filter' => new Filter([], 'node', $this->fieldManager),
+        'sort' => new Sort([]),
+        'page' => new OffsetPage([]),
+        // 'include' => new IncludeParam([]),
+        // 'fields' => new Fields([]),.
+      ],
+      RouteObjectInterface::ROUTE_OBJECT => $this->currentRoute,
+    ]));
+
+    $this->routeMatcher = new CurrentRouteMatch($this->requestStack);
+  }
+
+  /**
+   * @covers ::getResourceType
+   */
+  public function testGetResourceType() {
+    $request_context = new CurrentContext($this->resourceTypeRepository, $this->requestStack, $this->routeMatcher);
+
+    $this->assertEquals(
+      $this->resourceTypeRepository->get('node', 'article'),
+      $request_context->getResourceType()
+    );
+  }
+
+  /**
+   * @covers ::getJsonApiParameter
+   */
+  public function testGetJsonApiParameter() {
+    $request_context = new CurrentContext($this->resourceTypeRepository, $this->requestStack, $this->routeMatcher);
+
+    $expected = new Sort([]);
+    $actual = $request_context->getJsonApiParameter('sort');
+
+    $this->assertEquals($expected, $actual);
+  }
+
+  /**
+   * @covers ::hasExtension
+   */
+  public function testHasExtensionWithExistingExtension() {
+    $request = new Request();
+    $request->headers->set('Content-Type', 'application/vnd.api+json; ext="ext1,ext2"');
+    $this->requestStack->push($request);
+    $request_context = new CurrentContext($this->resourceTypeRepository, $this->requestStack, $this->routeMatcher);
+
+    $this->assertTrue($request_context->hasExtension('ext1'));
+    $this->assertTrue($request_context->hasExtension('ext2'));
+  }
+
+  /**
+   * @covers ::getExtensions
+   */
+  public function testGetExtensions() {
+    $request = new Request();
+    $request->headers->set('Content-Type', 'application/vnd.api+json; ext="ext1,ext2"');
+    $this->requestStack->push($request);
+    $request_context = new CurrentContext($this->resourceTypeRepository, $this->requestStack, $this->routeMatcher);
+
+    $this->assertEquals(['ext1', 'ext2'], $request_context->getExtensions());
+  }
+
+  /**
+   * @covers ::hasExtension
+   */
+  public function testHasExtensionWithNotExistingExtension() {
+    $request = new Request();
+    $request->headers->set('Content-Type', 'application/vnd.api+json;');
+    $this->requestStack->push($request);
+    $request_context = new CurrentContext($this->resourceTypeRepository, $this->requestStack, $this->routeMatcher);
+    $this->assertFalse($request_context->hasExtension('ext1'));
+    $this->assertFalse($request_context->hasExtension('ext2'));
+  }
+
+}
diff --git a/drupal/modules/jsonapi/tests/src/Unit/Controller/RequestHandlerTest.php b/drupal/modules/jsonapi/tests/src/Unit/Controller/RequestHandlerTest.php
new file mode 100644
index 0000000..4e1ee64
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Unit/Controller/RequestHandlerTest.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Unit\Controller;
+
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\jsonapi\ResourceType\ResourceType;
+use Drupal\jsonapi\Context\CurrentContext;
+use Drupal\jsonapi\Controller\RequestHandler;
+use Drupal\Tests\UnitTestCase;
+use Prophecy\Argument;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\HttpException;
+use Symfony\Component\Serializer\Exception\UnexpectedValueException;
+use Symfony\Component\Serializer\SerializerInterface;
+
+/**
+ * @coversDefaultClass \Drupal\jsonapi\Controller\RequestHandler
+ * @group jsonapi
+ */
+class RequestHandlerTest extends UnitTestCase {
+
+  /**
+   * @covers ::deserializeBody
+   * @expectedException \Symfony\Component\HttpKernel\Exception\HttpException
+   * @expectedExceptionMessageRegExp "There was an error un-serializing the data\..*"
+   */
+  public function testDeserializeBodyFail() {
+    $entity_storage = $this->prophesize(EntityStorageInterface::class);
+    $request_handler = new RequestHandler($entity_storage->reveal());
+    $request = $this->prophesize(Request::class);
+    $request->getContentType()->willReturn(NULL);
+    $request->getContent()->willReturn('this is not used');
+    $request->isMethodSafe()->willReturn(FALSE);
+    $request->getMethod()->willReturn(NULL);
+    $request->get(Argument::any())->willReturn(NULL);
+    $request->getMimeType(Argument::any())->willReturn(NULL);
+    $serializer = $this->prophesize(SerializerInterface::class);
+    $serializer->deserialize(Argument::type('string'), Argument::type('string'), Argument::any(), Argument::type('array'))
+      ->willThrow(new UnexpectedValueException('Foo'));
+    $serializer->serialize(Argument::any(), Argument::any(), Argument::any())
+      ->willReturn('{"errors":[{"status":422,"message":"Foo"}]}');
+    $current_context = $this->prophesize(CurrentContext::class);
+    $current_context->getResourceType()
+      ->willReturn(new ResourceType($this->randomMachineName(), $this->randomMachineName(), NULL));
+    try {
+      $request_handler->deserializeBody(
+        $request->reveal(),
+        $serializer->reveal(),
+        'invalid',
+        $current_context->reveal()
+      );
+      $this->fail('Expected exception.');
+    }
+    catch (HttpException $e) {
+      $this->assertEquals(422, $e->getStatusCode());
+      // Re-throw the exception so the test runner can catch it.
+      throw $e;
+    }
+  }
+
+}
diff --git a/drupal/modules/jsonapi/tests/src/Unit/EventSubscriber/ResourceResponseSubscriberTest.php b/drupal/modules/jsonapi/tests/src/Unit/EventSubscriber/ResourceResponseSubscriberTest.php
new file mode 100644
index 0000000..823b11d
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Unit/EventSubscriber/ResourceResponseSubscriberTest.php
@@ -0,0 +1,107 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Unit\EventSubscriber;
+
+use Drupal\Core\Render\RendererInterface;
+use Drupal\jsonapi\EventSubscriber\ResourceResponseSubscriber;
+use Drupal\rest\ResourceResponse;
+use Drupal\Tests\UnitTestCase;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Serializer\Serializer;
+
+/**
+ * @coversDefaultClass \Drupal\jsonapi\EventSubscriber\ResourceResponseSubscriber
+ * @group jsonapi
+ */
+class ResourceResponseSubscriberTest extends UnitTestCase {
+
+  /**
+   * @covers ::validateResponse
+   */
+  public function testValidateResponse() {
+    $resource_response_subscriber = new ResourceResponseSubscriber(
+      $this->prophesize(Serializer::class)->reveal(),
+      $this->prophesize(RendererInterface::class)->reveal(),
+      $this->prophesize(LoggerInterface::class)->reveal()
+    );
+
+    // Check that the validation class is enabled.
+    $this->assertTrue(
+      class_exists("\\JsonSchema\\Validator"),
+      'The JSON Schema validator is not present. Please make sure to install it using composer.'
+    );
+
+    // Expose protected ResourceResponseSubscriber::validateResponse() method.
+    $object = new \ReflectionObject($resource_response_subscriber);
+    $validate_response = $object->getMethod('validateResponse');
+    $validate_response->setAccessible(TRUE);
+
+    // Test validation failure: no "type" in "data".
+    $json = <<<'EOD'
+{
+  "data": {
+    "id": "4f342419-e668-4b76-9f87-7ce20c436169",
+    "attributes": {
+      "nid": "1",
+      "uuid": "4f342419-e668-4b76-9f87-7ce20c436169"
+    }
+  }
+}
+EOD;
+    $response = new ResourceResponse();
+    $response->setContent($json);
+    $this->assertFalse(
+      $validate_response->invoke($resource_response_subscriber, $response),
+      'Response validation failed to flag an invalid response.'
+    );
+
+    // Test validation failure: no "data" and "errors" at the root level.
+    $json = <<<'EOD'
+{
+  "data": {
+    "type": "node--article",
+    "id": "4f342419-e668-4b76-9f87-7ce20c436169",
+    "attributes": {
+      "nid": "1",
+      "uuid": "4f342419-e668-4b76-9f87-7ce20c436169"
+    }
+  },
+  "errors": [{}]
+}
+EOD;
+    $response = new ResourceResponse();
+    $response->setContent($json);
+    $this->assertFalse(
+      $validate_response->invoke($resource_response_subscriber, $response),
+      'Response validation failed to flag an invalid response.'
+    );
+
+    // Test validation success.
+    $json = <<<'EOD'
+{
+  "data": {
+    "type": "node--article",
+    "id": "4f342419-e668-4b76-9f87-7ce20c436169",
+    "attributes": {
+      "nid": "1",
+      "uuid": "4f342419-e668-4b76-9f87-7ce20c436169"
+    }
+  }
+}
+EOD;
+    $response->setContent($json);
+    $this->assertTrue(
+      $validate_response->invoke($resource_response_subscriber, $response),
+      'Response validation flagged a valid response.'
+    );
+
+    // Test validation of an empty response passes.
+    $response = new ResourceResponse();
+    $this->assertTrue(
+      $validate_response->invoke($resource_response_subscriber, $response),
+      'Response validation flagged a valid empty response.'
+    );
+
+  }
+
+}
diff --git a/drupal/modules/jsonapi/tests/src/Unit/JsonApiSpecTest.php b/drupal/modules/jsonapi/tests/src/Unit/JsonApiSpecTest.php
new file mode 100644
index 0000000..0b50191
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Unit/JsonApiSpecTest.php
@@ -0,0 +1,114 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Unit;
+
+use Drupal\jsonapi\JsonApiSpec;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\jsonapi\JsonApiSpec
+ * @group jsonapi
+ */
+class JsonApiSpecTest extends UnitTestCase {
+
+  /**
+   * @dataProvider providerTestIsValidMemberName
+   * @covers ::isValidMemberName
+   */
+  public function testIsValidMemberName($member_name, $expected) {
+    $this->assertSame($expected, JsonApiSpec::isValidMemberName($member_name));
+  }
+
+  public function providerTestIsValidMemberName() {
+    // Copied from http://jsonapi.org/format/upcoming/#document-member-names.
+    $data = [];
+    $data['alphanumeric-lowercase'] = ['12kittens', TRUE];
+    $data['alphanumeric-uppercase'] = ['12KITTENS', TRUE];
+    $data['alphanumeric-mixed'] = ['12KiTtEnS', TRUE];
+    $data['unicode-above-u+0080'] = ['12🐱🐱', TRUE];
+    $data['hyphen-start'] = ['-kittens', FALSE];
+    $data['hyphen-middle'] = ['kitt-ens', TRUE];
+    $data['hyphen-end'] = ['kittens-', FALSE];
+    $data['lowline-start'] = ['_kittens', FALSE];
+    $data['lowline-middle'] = ['kitt_ens', TRUE];
+    $data['lowline-end'] = ['kittens_', FALSE];
+    $data['space-start'] = [' kittens', FALSE];
+    $data['space-middle'] = ['kitt ens', TRUE];
+    $data['space-end'] = ['kittens ', FALSE];
+
+    // Additional test cases.
+    // @todo When D8 requires PHP >= 7, convert to \u{10FFFF}.
+    $data['unicode-above-u+0080-highest-allowed'] = ["12􏿿", TRUE];
+    $data['single-character'] = ['a', TRUE];
+
+    $unsafe_chars = [
+      '+',
+      ',',
+      '.',
+      '[',
+      ']',
+      '!',
+      '"',
+      '#',
+      '$',
+      '%',
+      '&',
+      '\'',
+      '(',
+      ')',
+      '*',
+      '/',
+      ':',
+      ';',
+      '<',
+      '=',
+      '>',
+      '?',
+      '@',
+      '\\',
+      '^',
+      '`',
+      '{',
+      '|',
+      '}',
+      '~',
+    ];
+    foreach ($unsafe_chars as $unsafe_char) {
+      $data['unsafe-' . $unsafe_char] = ['kitt' . $unsafe_char . 'ens', FALSE];
+    }
+
+    // The ASCII control characters are in the range 0x00 to 0x1F plus 0x7F.
+    for ($ascii = 0; $ascii <= 0x1F; $ascii++) {
+      $data['unsafe-ascii-control-' . $ascii] = ['kitt' . chr($ascii) . 'ens', FALSE];
+    }
+    $data['unsafe-ascii-control-' . 0x7F] = ['kitt' . chr(0x7F) . 'ens', FALSE];
+
+    return $data;
+  }
+
+  /**
+   * @dataProvider providerTestIsValidCustomQueryParameter
+   * @covers ::isValidCustomQueryParameter
+   * @covers ::isValidMemberName
+   */
+  public function testIsValidCustomQueryParameter($custom_query_parameter, $expected) {
+    $this->assertSame($expected, JsonApiSpec::isValidCustomQueryParameter($custom_query_parameter));
+  }
+
+  public function providerTestIsValidCustomQueryParameter() {
+    $data = $this->providerTestIsValidMemberName();
+
+    // All valid member names are also valid custom query parameters, except for
+    // single-character ones.
+    $data['single-character'][1] = FALSE;
+
+    // Custom query parameter test cases.
+    $data['custom-query-parameter-lowercase'] = ['foobar', FALSE];
+    $data['custom-query-parameter-dash'] = ['foo-bar', TRUE];
+    $data['custom-query-parameter-underscore'] = ['foo_bar', TRUE];
+    $data['custom-query-parameter-camelcase'] = ['fooBar', TRUE];
+
+    return $data;
+  }
+
+}
diff --git a/drupal/modules/jsonapi/tests/src/Unit/LinkManager/LinkManagerTest.php b/drupal/modules/jsonapi/tests/src/Unit/LinkManager/LinkManagerTest.php
new file mode 100644
index 0000000..7f0f416
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Unit/LinkManager/LinkManagerTest.php
@@ -0,0 +1,187 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Unit\LinkManager;
+
+use Drupal\Core\Routing\UrlGeneratorInterface;
+use Drupal\jsonapi\LinkManager\LinkManager;
+use Drupal\jsonapi\Routing\Param\OffsetPage;
+use Drupal\Tests\UnitTestCase;
+use Prophecy\Argument;
+use Symfony\Cmf\Component\Routing\ChainRouterInterface;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\HttpFoundation\ParameterBag;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * @coversDefaultClass \Drupal\jsonapi\LinkManager\LinkManager
+ * @group jsonapi
+ */
+class LinkManagerTest extends UnitTestCase {
+
+  /**
+   * The SUT.
+   *
+   * @var \Drupal\jsonapi\LinkManager\LinkManager
+   */
+  protected $linkManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $router = $this->prophesize(ChainRouterInterface::class);
+    $router->matchRequest(Argument::type(Request::class))->willReturn([
+      RouteObjectInterface::ROUTE_NAME => 'fake',
+      '_raw_variables' => new ParameterBag(['lorem' => 'ipsum']),
+    ]);
+    $url_generator = $this->prophesize(UrlGeneratorInterface::class);
+    $url_generator->generateFromRoute(Argument::cetera())->willReturnArgument(2);
+    $this->linkManager = new LinkManager($router->reveal(), $url_generator->reveal());
+  }
+
+  /**
+   * @covers ::getPagerLinks
+   * @dataProvider getPagerLinksProvider
+   */
+  public function testGetPagerLinks($offset, $size, $has_next_page, $total, $include_count, array $pages) {
+    // Add the extra stuff to the expected query.
+    $pages = array_filter($pages);
+    $pages = array_map(function ($page) {
+      return ['absolute' => TRUE, 'query' => ['page' => $page]];
+    }, $pages);
+
+    $request = $this->prophesize(Request::class);
+    // Have the request return the desired page parameter.
+    $page_param = $this->prophesize(OffsetPage::class);
+    $page_param->getOffset()->willReturn($offset);
+    $page_param->getSize()->willReturn($size);
+    $request->get('_json_api_params')->willReturn(['page' => $page_param->reveal()]);
+    $request->query = new ParameterBag();
+
+    $context = ['has_next_page' => $has_next_page];
+    if ($include_count) {
+      $context['total_count'] = $total;
+    }
+
+    $links = $this->linkManager
+      ->getPagerLinks($request->reveal(), $context);
+    $this->assertEquals($pages, $links);
+  }
+
+  /**
+   * Data provider for testGetPagerLinks.
+   *
+   * @return array
+   *   The data for the test method.
+   */
+  public function getPagerLinksProvider() {
+    return [
+      [1, 4, TRUE, 8, TRUE, [
+        'first' => ['offset' => 0, 'limit' => 4],
+        'prev' => ['offset' => 0, 'limit' => 4],
+        'next' => ['offset' => 5, 'limit' => 4],
+        'last' => ['offset' => 4, 'limit' => 4],
+      ],
+      ],
+      [6, 4, FALSE, 4, TRUE, [
+        'first' => ['offset' => 0, 'limit' => 4],
+        'prev' => ['offset' => 2, 'limit' => 4],
+        'next' => NULL,
+      ],
+      ],
+      [7, 4, FALSE, 5, FALSE, [
+        'first' => ['offset' => 0, 'limit' => 4],
+        'prev' => ['offset' => 3, 'limit' => 4],
+        'next' => NULL,
+      ],
+      ],
+      [10, 4, FALSE, 20, FALSE, [
+        'first' => ['offset' => 0, 'limit' => 4],
+        'prev' => ['offset' => 6, 'limit' => 4],
+        'next' => NULL,
+      ],
+      ],
+      [5, 4, TRUE, 30, FALSE, [
+        'first' => ['offset' => 0, 'limit' => 4],
+        'prev' => ['offset' => 1, 'limit' => 4],
+        'next' => ['offset' => 9, 'limit' => 4],
+      ],
+      ],
+      [0, 4, TRUE, 100, TRUE, [
+        'first' => NULL,
+        'prev' => NULL,
+        'next' => ['offset' => 4, 'limit' => 4],
+        'last' => ['offset' => 96, 'limit' => 4],
+      ],
+      ],
+      [0, 1, FALSE, 1, FALSE, [
+        'first' => NULL,
+        'prev' => NULL,
+        'next' => NULL,
+      ],
+      ],
+      [0, 1, FALSE, 2, FALSE, [
+        'first' => NULL,
+        'prev' => NULL,
+        'next' => NULL,
+      ],
+      ],
+    ];
+  }
+
+  /**
+   * Test errors.
+   *
+   * @covers ::getPagerLinks
+   * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
+   * @dataProvider getPagerLinksErrorProvider
+   */
+  public function testGetPagerLinksError($offset, $size, $has_next_page, $total, $include_count, array $pages) {
+    $this->testGetPagerLinks($offset, $size, $has_next_page, $total, $include_count, $pages);
+  }
+
+  /**
+   * Data provider for testGetPagerLinksError.
+   *
+   * @return array
+   *   The data for the test method.
+   */
+  public function getPagerLinksErrorProvider() {
+    return [
+      [0, -5, FALSE, 10, TRUE, [
+          'first' => NULL,
+          'prev' => NULL,
+          'last' => NULL,
+          'next' => NULL,
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * @covers ::getRequestLink
+   */
+  public function testGetRequestLink() {
+    $request = $this->prophesize(Request::class);
+    // Have the request return the desired page parameter.
+    $page_param = $this->prophesize(OffsetPage::class);
+    $page_param->getOffset()->willReturn(NULL);
+    $page_param->getSize()->willReturn(NULL);
+    $request->get('_json_api_params')->willReturn(['page' => $page_param->reveal()]);
+    $request->query = new ParameterBag(['amet' => 'pax']);
+
+    $query = $this->linkManager->getRequestLink($request->reveal(), ['dolor' => 'sid']);
+    $this->assertEquals([
+      'absolute' => TRUE,
+      'query' => ['dolor' => 'sid'],
+    ], $query);
+    // Get the default query from the request object.
+    $query = $this->linkManager->getRequestLink($request->reveal());
+    $this->assertEquals([
+      'absolute' => TRUE,
+      'query' => ['amet' => 'pax'],
+    ], $query);
+  }
+
+}
diff --git a/drupal/modules/jsonapi/tests/src/Unit/Normalizer/ConfigEntityNormalizerTest.php b/drupal/modules/jsonapi/tests/src/Unit/Normalizer/ConfigEntityNormalizerTest.php
new file mode 100644
index 0000000..732dca0
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Unit/Normalizer/ConfigEntityNormalizerTest.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Unit\Normalizer;
+
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\jsonapi\ResourceType\ResourceType;
+use Drupal\jsonapi\ResourceType\ResourceTypeRepository;
+use Drupal\jsonapi\Normalizer\ConfigEntityNormalizer;
+use Drupal\jsonapi\LinkManager\LinkManager;
+use Drupal\jsonapi\Normalizer\ScalarNormalizer;
+use Drupal\Tests\UnitTestCase;
+use Prophecy\Argument;
+use Symfony\Component\Serializer\Serializer;
+
+/**
+ * @coversDefaultClass \Drupal\jsonapi\Normalizer\ConfigEntityNormalizer
+ * @group jsonapi
+ */
+class ConfigEntityNormalizerTest extends UnitTestCase {
+
+  /**
+   * The normalizer under test.
+   *
+   * @var \Drupal\jsonapi\Normalizer\ConfigEntityNormalizer
+   */
+  protected $normalizer;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    $link_manager = $this->prophesize(LinkManager::class);
+
+    $resource_type_repository = $this->prophesize(ResourceTypeRepository::class);
+    $resource_type_repository->get(Argument::type('string'), Argument::type('string'))
+      ->willReturn(new ResourceType('dolor', 'sid', NULL));
+
+    $this->normalizer = new ConfigEntityNormalizer(
+      $link_manager->reveal(),
+      $resource_type_repository->reveal(),
+      $this->prophesize(EntityTypeManagerInterface::class)->reveal()
+    );
+
+    $normalizers = [new ScalarNormalizer()];
+    $serializer = new Serializer($normalizers, []);
+    $this->normalizer->setSerializer($serializer);
+  }
+
+  /**
+   * @covers ::normalize
+   * @dataProvider normalizeProvider
+   */
+  public function testNormalize($input, $expected) {
+    $entity = $this->prophesize(ConfigEntityInterface::class);
+    $entity->toArray()->willReturn(['amet' => $input]);
+    $entity->getCacheContexts()->willReturn([]);
+    $entity->getCacheTags()->willReturn([]);
+    $entity->getCacheMaxAge()->willReturn(-1);
+    $entity->getEntityTypeId()->willReturn('');
+    $entity->bundle()->willReturn('');
+    $normalized = $this->normalizer->normalize($entity->reveal(), 'api_json', []);
+    $first = $normalized->getValues();
+    $first = reset($first);
+    $this->assertSame($expected, $first->rasterizeValue());
+  }
+
+  /**
+   * Data provider for the normalize test.
+   *
+   * @return array
+   *   The data for the test method.
+   */
+  public function normalizeProvider() {
+    return [
+      ['lorem', 'lorem'],
+      [
+        ['ipsum' => 'dolor', 'ra' => 'foo'],
+        ['ipsum' => 'dolor', 'ra' => 'foo'],
+      ],
+      [['ipsum' => 'dolor'], 'dolor'],
+      [
+        ['lorem' => ['ipsum' => ['dolor' => 'sid', 'amet' => 'ra']]],
+        ['ipsum' => ['dolor' => 'sid', 'amet' => 'ra']],
+      ],
+    ];
+  }
+
+}
diff --git a/drupal/modules/jsonapi/tests/src/Unit/Normalizer/EntityReferenceFieldNormalizerTest.php b/drupal/modules/jsonapi/tests/src/Unit/Normalizer/EntityReferenceFieldNormalizerTest.php
new file mode 100644
index 0000000..4bf2490
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Unit/Normalizer/EntityReferenceFieldNormalizerTest.php
@@ -0,0 +1,159 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Unit\Normalizer;
+
+use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityRepositoryInterface;
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Field\FieldTypePluginManagerInterface;
+use Drupal\Core\Field\TypedData\FieldItemDataDefinition;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\jsonapi\ResourceType\ResourceType;
+use Drupal\jsonapi\ResourceType\ResourceTypeRepository;
+use Drupal\jsonapi\Normalizer\EntityReferenceFieldNormalizer;
+use Drupal\jsonapi\LinkManager\LinkManager;
+use Drupal\Tests\UnitTestCase;
+use Prophecy\Argument;
+
+/**
+ * @coversDefaultClass \Drupal\jsonapi\Normalizer\EntityReferenceFieldNormalizer
+ * @group jsonapi
+ */
+class EntityReferenceFieldNormalizerTest extends UnitTestCase {
+
+  /**
+   * The normalizer under test.
+   *
+   * @var \Drupal\jsonapi\Normalizer\EntityReferenceFieldNormalizer
+   */
+  protected $normalizer;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    $link_manager = $this->prophesize(LinkManager::class);
+    $field_manager = $this->prophesize(EntityFieldManagerInterface::class);
+    $field_definition = $this->prophesize(FieldConfig::class);
+    $item_definition = $this->prophesize(FieldItemDataDefinition::class);
+    $item_definition->getMainPropertyName()->willReturn('bunny');
+    $item_definition->getSetting('target_type')->willReturn('fake_entity_type');
+    $item_definition->getSetting('handler_settings')->willReturn([
+      'target_bundles' => ['dummy_bundle'],
+    ]);
+    $field_definition->getItemDefinition()
+      ->willReturn($item_definition->reveal());
+    $storage_definition = $this->prophesize(FieldStorageDefinitionInterface::class);
+    $storage_definition->isMultiple()->willReturn(TRUE);
+    $field_definition->getFieldStorageDefinition()->willReturn($storage_definition->reveal());
+
+    $field_definition2 = $this->prophesize(FieldConfig::class);
+    $field_definition2->getItemDefinition()
+      ->willReturn($item_definition->reveal());
+    $storage_definition2 = $this->prophesize(FieldStorageDefinitionInterface::class);
+    $storage_definition2->isMultiple()->willReturn(FALSE);
+    $field_definition2->getFieldStorageDefinition()->willReturn($storage_definition2->reveal());
+
+    $field_manager->getFieldDefinitions('fake_entity_type', 'dummy_bundle')
+      ->willReturn([
+        'field_dummy' => $field_definition->reveal(),
+        'field_dummy_single' => $field_definition2->reveal(),
+      ]);
+    $plugin_manager = $this->prophesize(FieldTypePluginManagerInterface::class);
+    $plugin_manager->createFieldItemList(
+      Argument::type(FieldableEntityInterface::class),
+      Argument::type('string'),
+      Argument::type('array')
+    )->willReturnArgument(2);
+    $resource_type_repository = $this->prophesize(ResourceTypeRepository::class);
+    $resource_type_repository->get('fake_entity_type', 'dummy_bundle')
+      ->willReturn(new ResourceType('lorem', 'dummy_bundle', NULL));
+
+    $entity = $this->prophesize(EntityInterface::class);
+    $entity->uuid()->willReturn('4e6cb61d-4f04-437f-99fe-42c002393658');
+    $entity->id()->willReturn(42);
+    $entity_repository = $this->prophesize(EntityRepositoryInterface::class);
+    $entity_repository->loadEntityByUuid('lorem', '4e6cb61d-4f04-437f-99fe-42c002393658')
+      ->willReturn($entity->reveal());
+
+    $this->normalizer = new EntityReferenceFieldNormalizer(
+      $link_manager->reveal(),
+      $field_manager->reveal(),
+      $plugin_manager->reveal(),
+      $resource_type_repository->reveal(),
+      $entity_repository->reveal()
+    );
+  }
+
+  /**
+   * @covers ::denormalize
+   * @dataProvider denormalizeProvider
+   */
+  public function testDenormalize($input, $field_name, $expected) {
+    $entity = $this->prophesize(FieldableEntityInterface::class);
+    $context = [
+      'resource_type' => new ResourceType('fake_entity_type', 'dummy_bundle', NULL),
+      'related' => $field_name,
+      'target_entity' => $entity->reveal(),
+    ];
+    $denormalized = $this->normalizer->denormalize($input, NULL, 'api_json', $context);
+    $this->assertSame($expected, $denormalized);
+  }
+
+  /**
+   * Data provider for the denormalize test.
+   *
+   * @return array
+   *   The data for the test method.
+   */
+  public function denormalizeProvider() {
+    return [
+      [
+        ['data' => [['type' => 'lorem--dummy_bundle', 'id' => '4e6cb61d-4f04-437f-99fe-42c002393658']]],
+        'field_dummy',
+        [['bunny' => 42]],
+      ],
+      [
+        ['data' => []],
+        'field_dummy',
+        [],
+      ],
+      [
+        ['data' => NULL],
+        'field_dummy_single',
+        [],
+      ],
+    ];
+  }
+
+  /**
+   * @covers ::denormalize
+   * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
+   * @dataProvider denormalizeInvalidResourceProvider
+   */
+  public function testDenormalizeInvalidResource($data, $field_name) {
+    $context = [
+      'resource_type' => new ResourceType('fake_entity_type', 'dummy_bundle', NULL),
+      'related' => $field_name,
+      'target_entity' => $this->prophesize(FieldableEntityInterface::class)->reveal(),
+    ];
+    $this->normalizer->denormalize($data, NULL, 'api_json', $context);
+  }
+
+  /**
+   * Data provider for the denormalize test.
+   *
+   * @return array
+   *   The input data for the test method.
+   */
+  public function denormalizeInvalidResourceProvider() {
+    return [
+      [['data' => [['type' => 'invalid', 'id' => '4e6cb61d-4f04-437f-99fe-42c002393658']]], 'field_dummy'],
+      [['data' => ['type' => 'lorem', 'id' => '4e6cb61d-4f04-437f-99fe-42c002393658']], 'field_dummy'],
+      [['data' => [['type' => 'lorem', 'id' => '4e6cb61d-4f04-437f-99fe-42c002393658']]], 'field_dummy_single'],
+    ];
+  }
+
+}
diff --git a/drupal/modules/jsonapi/tests/src/Unit/Normalizer/HttpExceptionNormalizerTest.php b/drupal/modules/jsonapi/tests/src/Unit/Normalizer/HttpExceptionNormalizerTest.php
new file mode 100644
index 0000000..f72db5c
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Unit/Normalizer/HttpExceptionNormalizerTest.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Unit\Normalizer;
+
+use Drupal\Core\Session\AccountProxyInterface;
+use Drupal\jsonapi\Normalizer\HttpExceptionNormalizer;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
+
+/**
+ * @coversDefaultClass \Drupal\jsonapi\Normalizer\HttpExceptionNormalizer
+ * @group jsonapi
+ */
+class HttpExceptionNormalizerTest extends UnitTestCase {
+
+  /**
+   * @covers ::normalize
+   */
+  public function testNormalize() {
+    $exception = new AccessDeniedHttpException('lorem', NULL, 13);
+    $current_user = $this->prophesize(AccountProxyInterface::class);
+    $current_user->hasPermission('access site reports')->willReturn(TRUE);
+    $normalizer = new HttpExceptionNormalizer($current_user->reveal());
+    $normalized = $normalizer->normalize($exception, 'api_json');
+    $normalized = $normalized->rasterizeValue();
+    $error = $normalized[0];
+    $this->assertNotEmpty($error['meta']);
+    $this->assertNotEmpty($error['source']);
+    $this->assertEquals(13, $error['code']);
+    $this->assertEquals(403, $error['status']);
+    $this->assertEquals('Forbidden', $error['title']);
+    $this->assertEquals('lorem', $error['detail']);
+    $this->assertNull($error['meta']['trace'][1]['args'][0]);
+
+    $current_user = $this->prophesize(AccountProxyInterface::class);
+    $current_user->hasPermission('access site reports')->willReturn(FALSE);
+    $normalizer = new HttpExceptionNormalizer($current_user->reveal());
+    $normalized = $normalizer->normalize($exception, 'api_json');
+    $normalized = $normalized->rasterizeValue();
+    $error = $normalized[0];
+    $this->assertTrue(empty($error['meta']));
+    $this->assertTrue(empty($error['source']));
+  }
+
+}
diff --git a/drupal/modules/jsonapi/tests/src/Unit/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php b/drupal/modules/jsonapi/tests/src/Unit/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php
new file mode 100644
index 0000000..42bdff8
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Unit/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php
@@ -0,0 +1,177 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Unit\Normalizer;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\jsonapi\ResourceType\ResourceType;
+use Drupal\jsonapi\Normalizer\JsonApiDocumentTopLevelNormalizer;
+use Drupal\jsonapi\LinkManager\LinkManager;
+use Drupal\jsonapi\Context\CurrentContext;
+use Drupal\Tests\UnitTestCase;
+use Prophecy\Argument;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Serializer\SerializerInterface;
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
+use Drupal\jsonapi\ResourceType\ResourceTypeRepository;
+
+/**
+ * @coversDefaultClass \Drupal\jsonapi\Normalizer\JsonApiDocumentTopLevelNormalizer
+ * @group jsonapi
+ */
+class JsonApiDocumentTopLevelNormalizerTest extends UnitTestCase {
+
+  /**
+   * The normalizer under test.
+   *
+   * @var \Drupal\jsonapi\Normalizer\JsonApiDocumentTopLevelNormalizer
+   */
+  protected $normalizer;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    $link_manager = $this->prophesize(LinkManager::class);
+    $current_context_manager = $this->prophesize(CurrentContext::class);
+    $resource_type_repository = $this->prophesize(ResourceTypeRepository::class);
+
+    $resource_type = $this->prophesize(ResourceType::class);
+    $resource_type
+      ->getEntityTypeId()
+      ->willReturn('node');
+
+    $resource_type_repository
+      ->getByTypeName(Argument::any())
+      ->willReturn($resource_type->reveal());
+
+    $entity_storage = $this->prophesize(EntityStorageInterface::class);
+    $self = $this;
+    $uuid_to_id = [
+      '76dd5c18-ea1b-4150-9e75-b21958a2b836' => 1,
+      'fcce1b61-258e-4054-ae36-244d25a9e04c' => 2,
+    ];
+    $entity_storage->loadByProperties(Argument::type('array'))
+      ->will(function ($args) use ($self, $uuid_to_id) {
+        $result = [];
+        foreach ($args[0]['uuid'] as $uuid) {
+          $entity = $self->prophesize(EntityInterface::class);
+          $entity->uuid()->willReturn($uuid);
+          $entity->id()->willReturn($uuid_to_id[$uuid]);
+          $result[$uuid] = $entity->reveal();
+        }
+        return $result;
+      });
+    $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class);
+    $entity_type_manager->getStorage('node')
+      ->willReturn($entity_storage->reveal());
+
+    $current_route = $this->prophesize(Route::class);
+    $current_route->getDefault('_on_relationship')->willReturn(FALSE);
+
+    $current_context_manager->isOnRelationship()->willReturn(FALSE);
+
+    $this->normalizer = new JsonApiDocumentTopLevelNormalizer(
+      $link_manager->reveal(),
+      $current_context_manager->reveal(),
+      $entity_type_manager->reveal(),
+      $resource_type_repository->reveal()
+    );
+
+    $serializer = $this->prophesize(DenormalizerInterface::class);
+    $serializer->willImplement(SerializerInterface::class);
+    $serializer->denormalize(
+      Argument::type('array'),
+      Argument::type('string'),
+      Argument::type('string'),
+      Argument::type('array')
+    )->willReturnArgument(0);
+
+    $this->normalizer->setSerializer($serializer->reveal());
+  }
+
+  /**
+   * @covers ::denormalize
+   * @dataProvider denormalizeProvider
+   */
+  public function testDenormalize($input, $expected) {
+    $context = [
+      'resource_type' => new ResourceType($this->randomMachineName(), $this->randomMachineName(), FieldableEntityInterface::class),
+    ];
+    $denormalized = $this->normalizer->denormalize($input, NULL, 'api_json', $context);
+    $this->assertSame($expected, $denormalized);
+  }
+
+  /**
+   * Data provider for the denormalize test.
+   *
+   * @return array
+   *   The data for the test method.
+   */
+  public function denormalizeProvider() {
+    return [
+      [
+        [
+          'data' => [
+            'type' => 'lorem',
+            'id' => 'e1a613f6-f2b9-4e17-9d33-727eb6509d8b',
+            'attributes' => ['title' => 'dummy_title'],
+          ],
+        ],
+        ['title' => 'dummy_title'],
+      ],
+      [
+        [
+          'data' => [
+            'type' => 'lorem',
+            'id' => '0676d1bf-55b3-4bbc-9fbc-3df10f4599d5',
+            'relationships' => ['field_dummy' => ['data' => ['type' => 'node', 'id' => '76dd5c18-ea1b-4150-9e75-b21958a2b836']]],
+          ],
+        ],
+        ['field_dummy' => [
+          [
+            'target_id' => 1
+          ],
+        ]],
+      ],
+      [
+        [
+          'data' => [
+            'type' => 'lorem',
+            'id' => '535ba297-8d79-4fc1-b0d6-dc2f047765a1',
+            'relationships' => ['field_dummy' => ['data' => [['type' => 'node', 'id' => '76dd5c18-ea1b-4150-9e75-b21958a2b836'], ['type' => 'node', 'id' => 'fcce1b61-258e-4054-ae36-244d25a9e04c']]]],
+          ],
+        ],
+        ['field_dummy' => [
+          [
+            'target_id' => 1,
+          ],
+          [
+            'target_id' => 2,
+          ]
+        ]],
+      ],
+      [
+        [
+          'data' => [
+            'type' => 'lorem',
+            'id' => '535ba297-8d79-4fc1-b0d6-dc2f047765a1',
+            'relationships' => ['field_dummy' => ['data' => [['type' => 'node', 'id' => '76dd5c18-ea1b-4150-9e75-b21958a2b836', 'meta' => ['foo' => 'bar']], ['type' => 'node', 'id' => 'fcce1b61-258e-4054-ae36-244d25a9e04c']]]],
+          ],
+        ],
+        ['field_dummy' => [
+          [
+            'target_id' => 1,
+            'foo' => 'bar',
+          ],
+          [
+            'target_id' => 2,
+          ]
+        ]],
+      ],
+    ];
+  }
+
+}
diff --git a/drupal/modules/jsonapi/tests/src/Unit/Normalizer/Value/EntityNormalizerValueTest.php b/drupal/modules/jsonapi/tests/src/Unit/Normalizer/Value/EntityNormalizerValueTest.php
new file mode 100644
index 0000000..90b7ec1
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Unit/Normalizer/Value/EntityNormalizerValueTest.php
@@ -0,0 +1,156 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Unit\Normalizer\Value;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\jsonapi\ResourceType\ResourceType;
+use Drupal\jsonapi\LinkManager\LinkManager;
+use Drupal\jsonapi\Normalizer\Value\EntityNormalizerValue;
+use Drupal\jsonapi\Normalizer\Value\JsonApiDocumentTopLevelNormalizerValue;
+use Drupal\jsonapi\Normalizer\Value\RelationshipNormalizerValue;
+use Drupal\jsonapi\Normalizer\Value\FieldNormalizerValueInterface;
+use Drupal\node\NodeInterface;
+use Drupal\Tests\UnitTestCase;
+use Prophecy\Argument;
+
+/**
+ * @coversDefaultClass \Drupal\jsonapi\Normalizer\Value\EntityNormalizerValue
+ * @group jsonapi
+ */
+class EntityNormalizerValueTest extends UnitTestCase {
+
+  /**
+   * The EntityNormalizerValue object.
+   *
+   * @var \Drupal\jsonapi\Normalizer\Value\EntityNormalizerValue
+   */
+  protected $object;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $field1 = $this->prophesize(FieldNormalizerValueInterface::class);
+    $field1->getIncludes()->willReturn([]);
+    $field1->getPropertyType()->willReturn('attributes');
+    $field1->rasterizeValue()->willReturn('dummy_title');
+    $field2 = $this->prophesize(RelationshipNormalizerValue::class);
+    $field2->getPropertyType()->willReturn('relationships');
+    $field2->rasterizeValue()->willReturn(['data' => ['type' => 'node', 'id' => 2]]);
+    $included[] = $this->prophesize(JsonApiDocumentTopLevelNormalizerValue::class);
+    $included[0]->getIncludes()->willReturn([]);
+    $included[0]->rasterizeValue()->willReturn([
+      'data' => [
+        'type' => 'node',
+        'id' => '199c681d-a9dc-4b6f-a4dc-e3811f24141b',
+        'attributes' => ['body' => 'dummy_body1'],
+      ],
+    ]);
+    $included[0]->getCacheContexts()->willReturn(['lorem', 'ipsum']);
+    // Type & id duplicated on purpose.
+    $included[] = $this->prophesize(JsonApiDocumentTopLevelNormalizerValue::class);
+    $included[1]->getIncludes()->willReturn([]);
+    $included[1]->rasterizeValue()->willReturn([
+      'data' => [
+        'type' => 'node',
+        'id' => '199c681d-a9dc-4b6f-a4dc-e3811f24141b',
+        'attributes' => ['body' => 'dummy_body2'],
+      ],
+    ]);
+    $included[] = $this->prophesize(JsonApiDocumentTopLevelNormalizerValue::class);
+    $included[2]->getIncludes()->willReturn([]);
+    $included[2]->rasterizeValue()->willReturn([
+      'data' => [
+        'type' => 'node',
+        'id' => '83771375-a4ba-4d7d-a4d5-6153095bb5c5',
+        'attributes' => ['body' => 'dummy_body3'],
+      ],
+    ]);
+    $field2->getIncludes()->willReturn(array_map(function ($included_item) {
+      return $included_item->reveal();
+    }, $included));
+    $context = ['resource_type' => new ResourceType('node', 'article', NodeInterface::class)];
+    $entity = $this->prophesize(EntityInterface::class);
+    $entity->uuid()->willReturn('248150b2-79a2-4b44-9f49-bf405a51414a');
+    $entity->isNew()->willReturn(FALSE);
+    $entity->getEntityTypeId()->willReturn('node');
+    $entity->bundle()->willReturn('article');
+    $link_manager = $this->prophesize(LinkManager::class);
+    $link_manager
+      ->getEntityLink(Argument::any(), Argument::any(), Argument::type('array'), Argument::type('string'))
+      ->willReturn('dummy_entity_link');
+
+    // Stub the addCacheableDependency on the SUT. We'll test the cacheable
+    // metadata bubbling using Kernel tests.
+    $this->object = $this->getMockBuilder(EntityNormalizerValue::class)
+      ->setMethods(['addCacheableDependency'])
+      ->setConstructorArgs([
+        ['title' => $field1->reveal(), 'field_related' => $field2->reveal()],
+        $context,
+        $entity->reveal(),
+        ['link_manager' => $link_manager->reveal()],
+      ])
+      ->getMock();
+    $this->object->method('addCacheableDependency');
+  }
+
+  /**
+   * @covers ::rasterizeValue
+   */
+  public function testRasterizeValue() {
+    $this->assertEquals([
+      'type' => 'node--article',
+      'id' => '248150b2-79a2-4b44-9f49-bf405a51414a',
+      'attributes' => ['title' => 'dummy_title'],
+      'relationships' => [
+        'field_related' => ['data' => ['type' => 'node', 'id' => 2]],
+      ],
+      'links' => [
+        'self' => 'dummy_entity_link',
+      ],
+    ], $this->object->rasterizeValue());
+  }
+
+  /**
+   * @covers ::rasterizeIncludes
+   */
+  public function testRasterizeIncludes() {
+    $expected = [
+      [
+        'data' => [
+          'type' => 'node',
+          'id' => '199c681d-a9dc-4b6f-a4dc-e3811f24141b',
+          'attributes' => ['body' => 'dummy_body1'],
+        ],
+      ],
+      [
+        'data' => [
+          'type' => 'node',
+          'id' => '199c681d-a9dc-4b6f-a4dc-e3811f24141b',
+          'attributes' => ['body' => 'dummy_body2'],
+        ],
+      ],
+      [
+        'data' => [
+          'type' => 'node',
+          'id' => '83771375-a4ba-4d7d-a4d5-6153095bb5c5',
+          'attributes' => ['body' => 'dummy_body3'],
+        ],
+      ],
+    ];
+    $this->assertEquals($expected, $this->object->rasterizeIncludes());
+  }
+
+  /**
+   * @covers ::getIncludes
+   */
+  public function testGetIncludes() {
+    $includes = $this->object->getIncludes();
+    $includes = array_filter($includes, function ($included) {
+      return $included instanceof JsonApiDocumentTopLevelNormalizerValue;
+    });
+    $this->assertCount(3, $includes);
+  }
+
+}
diff --git a/drupal/modules/jsonapi/tests/src/Unit/Normalizer/Value/FieldItemNormalizerValueTest.php b/drupal/modules/jsonapi/tests/src/Unit/Normalizer/Value/FieldItemNormalizerValueTest.php
new file mode 100644
index 0000000..60bb2a4
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Unit/Normalizer/Value/FieldItemNormalizerValueTest.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Unit\Normalizer\Value;
+
+use Drupal\jsonapi\Normalizer\Value\FieldItemNormalizerValue;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\jsonapi\Normalizer\Value\FieldItemNormalizerValue
+ * @group jsonapi
+ */
+class FieldItemNormalizerValueTest extends UnitTestCase {
+
+  /**
+   * @covers ::rasterizeValue
+   * @dataProvider rasterizeValueProvider
+   */
+  public function testRasterizeValue($values, $expected) {
+    $object = new FieldItemNormalizerValue($values);
+    $this->assertEquals($expected, $object->rasterizeValue());
+  }
+
+  /**
+   * Provider for testRasterizeValue.
+   */
+  public function rasterizeValueProvider() {
+    return [
+      [['value' => 1], 1],
+      [['value' => 1, 'safe_value' => 1], ['value' => 1, 'safe_value' => 1]],
+      [[], []],
+      [[NULL], NULL],
+      [
+        [
+          'lorem' => [
+            'ipsum' => new FieldItemNormalizerValue([
+              'dolor' => 'sid',
+              'amet' => new FieldItemNormalizerValue(['value' => 'ra']),
+            ]),
+          ],
+        ],
+        ['ipsum' => ['dolor' => 'sid', 'amet' => 'ra']],
+      ],
+    ];
+  }
+
+}
diff --git a/drupal/modules/jsonapi/tests/src/Unit/Normalizer/Value/FieldNormalizerValueTest.php b/drupal/modules/jsonapi/tests/src/Unit/Normalizer/Value/FieldNormalizerValueTest.php
new file mode 100644
index 0000000..2c94b20
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Unit/Normalizer/Value/FieldNormalizerValueTest.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Unit\Normalizer\Value;
+
+use Drupal\jsonapi\Normalizer\Value\FieldItemNormalizerValue;
+use Drupal\jsonapi\Normalizer\Value\FieldNormalizerValue;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\jsonapi\Normalizer\Value\FieldNormalizerValue
+ * @group jsonapi
+ */
+class FieldNormalizerValueTest extends UnitTestCase {
+
+  /**
+   * @covers ::rasterizeValue
+   * @dataProvider rasterizeValueProvider
+   */
+  public function testRasterizeValue($values, $cardinality, $expected) {
+    $object = new FieldNormalizerValue($values, $cardinality);
+    $this->assertEquals($expected, $object->rasterizeValue());
+  }
+
+  /**
+   * Data provider for testRasterizeValue.
+   */
+  public function rasterizeValueProvider() {
+    $uuid_raw = '4ae99eec-8b0e-41f7-9400-fbd65c174902';
+    $uuid_value = $this->prophesize(FieldItemNormalizerValue::class);
+    $uuid_value->rasterizeValue()->willReturn('4ae99eec-8b0e-41f7-9400-fbd65c174902');
+    $uuid_value->getInclude()->willReturn(NULL);
+    return [
+      [[$uuid_value->reveal()], 1, $uuid_raw],
+      [[$uuid_value->reveal(), $uuid_value->reveal()], -1, [$uuid_raw, $uuid_raw]],
+    ];
+  }
+
+  /**
+   * @covers ::rasterizeIncludes
+   */
+  public function testRasterizeIncludes() {
+    $value = $this->prophesize(FieldItemNormalizerValue::class);
+    $include = $this->prophesize('\Drupal\jsonapi\Normalizer\Value\EntityNormalizerValue');
+    $include->rasterizeValue()->willReturn('Lorem');
+    $value->getInclude()->willReturn($include->reveal());
+    $object = new FieldNormalizerValue([$value->reveal()], 1);
+    $this->assertEquals(['Lorem'], $object->rasterizeIncludes());
+  }
+
+}
diff --git a/drupal/modules/jsonapi/tests/src/Unit/Normalizer/Value/JsonApiDocumentTopLevelNormalizerValueTest.php b/drupal/modules/jsonapi/tests/src/Unit/Normalizer/Value/JsonApiDocumentTopLevelNormalizerValueTest.php
new file mode 100644
index 0000000..55a77de
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Unit/Normalizer/Value/JsonApiDocumentTopLevelNormalizerValueTest.php
@@ -0,0 +1,121 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Unit\Normalizer\Value;
+
+use Drupal\Component\DependencyInjection\Container;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Url;
+use Drupal\jsonapi\ResourceType\ResourceType;
+use Drupal\jsonapi\LinkManager\LinkManager;
+use Drupal\jsonapi\Normalizer\Value\JsonApiDocumentTopLevelNormalizerValue;
+use Drupal\jsonapi\Normalizer\Value\RelationshipNormalizerValue;
+use Drupal\jsonapi\Normalizer\Value\FieldNormalizerValueInterface;
+use Drupal\node\NodeInterface;
+use Drupal\Tests\UnitTestCase;
+use Prophecy\Argument;
+
+/**
+ * @coversDefaultClass \Drupal\jsonapi\Normalizer\Value\JsonApiDocumentTopLevelNormalizerValue
+ * @group jsonapi
+ */
+class JsonApiDocumentTopLevelNormalizerValueTest extends UnitTestCase {
+
+  /**
+   * The JsonApiDocumentTopLevelNormalizerValue object.
+   *
+   * @var \Drupal\jsonapi\Normalizer\Value\JsonApiDocumentTopLevelNormalizerValue
+   */
+  protected $object;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $cache_contexts_manager = $this->getMockBuilder('Drupal\Core\Cache\Context\CacheContextsManager')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $cache_contexts_manager->method('assertValidTokens')->willReturn(TRUE);
+    $container = new Container();
+    $container->set('cache_contexts_manager', $cache_contexts_manager);
+    \Drupal::setContainer($container);
+
+    $field1 = $this->prophesize(FieldNormalizerValueInterface::class);
+    $field1->getIncludes()->willReturn([]);
+    $field1->getPropertyType()->willReturn('attributes');
+    $field1->rasterizeValue()->willReturn('dummy_title');
+    $field2 = $this->prophesize(RelationshipNormalizerValue::class);
+    $field2->getPropertyType()->willReturn('relationships');
+    $field2->rasterizeValue()->willReturn(['data' => ['type' => 'node', 'id' => 2]]);
+    $included[] = $this->prophesize(JsonApiDocumentTopLevelNormalizerValue::class);
+    $included[0]->getIncludes()->willReturn([]);
+    $included[0]->rasterizeValue()->willReturn([
+      'data' => [
+        'type' => 'node',
+        'id' => 3,
+        'attributes' => ['body' => 'dummy_body1'],
+      ],
+    ]);
+    $included[0]->getCacheContexts()->willReturn(['lorem:ipsum']);
+    // Type & id duplicated in purpose.
+    $included[] = $this->prophesize(JsonApiDocumentTopLevelNormalizerValue::class);
+    $included[1]->getIncludes()->willReturn([]);
+    $included[1]->rasterizeValue()->willReturn([
+      'data' => [
+        'type' => 'node',
+        'id' => 3,
+        'attributes' => ['body' => 'dummy_body2'],
+      ],
+    ]);
+    $included[] = $this->prophesize(JsonApiDocumentTopLevelNormalizerValue::class);
+    $included[2]->getIncludes()->willReturn([]);
+    $included[2]->rasterizeValue()->willReturn([
+      'data' => [
+        'type' => 'node',
+        'id' => 4,
+        'attributes' => ['body' => 'dummy_body3'],
+      ],
+    ]);
+    $field2->getIncludes()->willReturn(array_map(function ($included_item) {
+      return $included_item->reveal();
+    }, $included));
+    $context = ['resource_type' => new ResourceType('node', 'article', NodeInterface::class)];
+    $entity = $this->prophesize(EntityInterface::class);
+    $entity->id()->willReturn(1);
+    $entity->isNew()->willReturn(FALSE);
+    $entity->getEntityTypeId()->willReturn('node');
+    $entity->bundle()->willReturn('article');
+    $entity->hasLinkTemplate(Argument::type('string'))->willReturn(TRUE);
+    $url = $this->prophesize(Url::class);
+    $url->toString()->willReturn('dummy_entity_link');
+    $url->setRouteParameter(Argument::any(), Argument::any())->willReturn($url->reveal());
+    $entity->toUrl(Argument::type('string'), Argument::type('array'))->willReturn($url->reveal());
+    $link_manager = $this->prophesize(LinkManager::class);
+    $link_manager
+      ->getEntityLink(Argument::any(), Argument::any(), Argument::type('array'), Argument::type('string'))
+      ->willReturn('dummy_entity_link');
+    $this->object = $this->getMockBuilder(JsonApiDocumentTopLevelNormalizerValue::class)
+      ->setMethods(['addCacheableDependency'])
+      ->setConstructorArgs([
+        ['title' => $field1->reveal(), 'field_related' => $field2->reveal()],
+        $context,
+        $entity->reveal(),
+        ['link_manager' => $link_manager->reveal()],
+      ])
+      ->getMock();
+    $this->object->method('addCacheableDependency');
+  }
+
+  /**
+   * @covers ::getIncludes
+   */
+  public function testGetIncludes() {
+    $includes = $this->object->getIncludes();
+    $includes = array_filter($includes, function ($included) {
+      return $included instanceof JsonApiDocumentTopLevelNormalizerValue;
+    });
+    $this->assertCount(2, $includes);
+  }
+
+}
diff --git a/drupal/modules/jsonapi/tests/src/Unit/Normalizer/Value/RelationshipItemNormalizerValueTest.php b/drupal/modules/jsonapi/tests/src/Unit/Normalizer/Value/RelationshipItemNormalizerValueTest.php
new file mode 100644
index 0000000..f9f16a7
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Unit/Normalizer/Value/RelationshipItemNormalizerValueTest.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Unit\Normalizer\Value;
+
+use Drupal\jsonapi\ResourceType\ResourceType;
+use Drupal\jsonapi\Normalizer\Value\RelationshipItemNormalizerValue;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\jsonapi\Normalizer\Value\RelationshipItemNormalizerValue
+ * @group jsonapi
+ */
+class RelationshipItemNormalizerValueTest extends UnitTestCase {
+
+  /**
+   * @covers ::rasterizeValue
+   * @dataProvider rasterizeValueProvider
+   */
+  public function testRasterizeValue($values, $entity_type_id, $bundle, $expected) {
+    $object = new RelationshipItemNormalizerValue($values, new ResourceType($entity_type_id, $bundle, NULL));
+    $this->assertEquals($expected, $object->rasterizeValue());
+  }
+
+  /**
+   * Data provider for testRasterizeValue.
+   */
+  public function rasterizeValueProvider() {
+    return [
+      [['target_id' => 1], 'node', 'article', ['type' => 'node--article', 'id' => 1]],
+      [['value' => 1], 'node', 'page', ['type' => 'node--page', 'id' => 1]],
+      [[1], 'node', 'foo', ['type' => 'node--foo', 'id' => 1]],
+      [[], 'node', 'bar', []],
+      [[NULL], 'node', 'baz', NULL],
+    ];
+  }
+
+}
diff --git a/drupal/modules/jsonapi/tests/src/Unit/Normalizer/Value/RelationshipNormalizerValueTest.php b/drupal/modules/jsonapi/tests/src/Unit/Normalizer/Value/RelationshipNormalizerValueTest.php
new file mode 100644
index 0000000..57094d1
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Unit/Normalizer/Value/RelationshipNormalizerValueTest.php
@@ -0,0 +1,94 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Unit\Normalizer\Value;
+
+use Drupal\jsonapi\ResourceType\ResourceType;
+use Drupal\jsonapi\LinkManager\LinkManager;
+use Drupal\jsonapi\Normalizer\Value\RelationshipItemNormalizerValue;
+use Drupal\jsonapi\Normalizer\Value\RelationshipNormalizerValue;
+use Drupal\jsonapi\Normalizer\Value\FieldItemNormalizerValue;
+use Drupal\Tests\UnitTestCase;
+use Prophecy\Argument;
+
+/**
+ * @coversDefaultClass \Drupal\jsonapi\Normalizer\Value\RelationshipNormalizerValue
+ * @group jsonapi
+ */
+class RelationshipNormalizerValueTest extends UnitTestCase {
+
+  /**
+   * @covers ::rasterizeValue
+   * @dataProvider rasterizeValueProvider
+   */
+  public function testRasterizeValue($values, $cardinality, $expected) {
+    $link_manager = $this->prophesize(LinkManager::class);
+    $link_manager
+      ->getEntityLink(Argument::any(), Argument::any(), Argument::type('array'), Argument::type('string'))
+      ->willReturn('dummy_entity_link');
+    $object = new RelationshipNormalizerValue($values, $cardinality, [
+      'link_manager' => $link_manager->reveal(),
+      'host_entity_id' => 'lorem',
+      'resource_type' => new ResourceType($this->randomMachineName(), $this->randomMachineName(), NULL),
+      'field_name' => 'ipsum',
+    ]);
+    $this->assertEquals($expected, $object->rasterizeValue());
+  }
+
+  /**
+   * Data provider fortestRasterizeValue.
+   */
+  public function rasterizeValueProvider() {
+    $uid_raw = 1;
+    $uid1 = $this->prophesize(RelationshipItemNormalizerValue::class);
+    $uid1->rasterizeValue()->willReturn(['type' => 'user', 'id' => $uid_raw++]);
+    $uid1->getInclude()->willReturn(NULL);
+    $uid2 = $this->prophesize(RelationshipItemNormalizerValue::class);
+    $uid2->rasterizeValue()->willReturn(['type' => 'user', 'id' => $uid_raw]);
+    $uid2->getInclude()->willReturn(NULL);
+    $links = [
+      'self' => 'dummy_entity_link',
+      'related' => 'dummy_entity_link',
+    ];
+    return [
+      [[$uid1->reveal()], 1, [
+        'data' => ['type' => 'user', 'id' => 1],
+        'links' => $links,
+      ],
+      ],
+      [
+        [$uid1->reveal(), $uid2->reveal()], 2, [
+          'data' => [
+            ['type' => 'user', 'id' => 1],
+            ['type' => 'user', 'id' => 2],
+          ],
+          'links' => $links,
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * @covers ::rasterizeValue
+   *
+   * @expectedException \RuntimeException
+   */
+  public function testRasterizeValueFails() {
+    $uid1 = $this->prophesize(FieldItemNormalizerValue::class);
+    $uid1->rasterizeValue()->willReturn(1);
+    $uid1->getInclude()->willReturn(NULL);
+    $link_manager = $this->prophesize(LinkManager::class);
+    $link_manager
+      ->getEntityLink(Argument::any(), Argument::any(), Argument::type('array'), Argument::type('string'))
+      ->willReturn('dummy_entity_link');
+    $object = new RelationshipNormalizerValue([$uid1->reveal()], 1, [
+      'link_manager' => $link_manager->reveal(),
+      'host_entity_id' => 'lorem',
+      'resource_type' => new ResourceType($this->randomMachineName(), $this->randomMachineName(), NULL),
+      'field_name' => 'ipsum',
+    ]);
+    $object->rasterizeValue();
+    // If the exception was not thrown, then the following fails.
+    $this->assertTrue(FALSE);
+  }
+
+}
diff --git a/drupal/modules/jsonapi/tests/src/Unit/Routing/JsonApiParamEnhancerTest.php b/drupal/modules/jsonapi/tests/src/Unit/Routing/JsonApiParamEnhancerTest.php
new file mode 100644
index 0000000..060d40a
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Unit/Routing/JsonApiParamEnhancerTest.php
@@ -0,0 +1,96 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Unit\Routing;
+
+use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\jsonapi\Routing\JsonApiParamEnhancer;
+use Drupal\jsonapi\Routing\Param\OffsetPage;
+use Drupal\jsonapi\Routing\Param\Filter;
+use Drupal\jsonapi\Routing\Param\Sort;
+use Drupal\jsonapi\Routing\Routes;
+use Drupal\Tests\UnitTestCase;
+use Prophecy\Argument;
+use Prophecy\Promise\ReturnPromise;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\HttpFoundation\ParameterBag;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Route;
+
+/**
+ * @coversDefaultClass \Drupal\jsonapi\Routing\JsonApiParamEnhancer
+ * @group jsonapi
+ * @group jsonapi_param_enhancer
+ */
+class JsonApiParamEnhancerTest extends UnitTestCase {
+
+  /**
+   * @covers ::applies
+   */
+  public function testApplies() {
+    $object = new JsonApiParamEnhancer($this->prophesize(EntityFieldManagerInterface::class)->reveal());
+    $route = $this->prophesize(Route::class);
+    $route->getDefault(RouteObjectInterface::CONTROLLER_NAME)->will(new ReturnPromise([Routes::FRONT_CONTROLLER, 'lorem']));
+
+    $this->assertTrue($object->applies($route->reveal()));
+    $this->assertFalse($object->applies($route->reveal()));
+  }
+
+  /**
+   * @covers ::enhance
+   */
+  public function testEnhanceFilter() {
+    $object = new JsonApiParamEnhancer($this->prophesize(EntityFieldManagerInterface::class)->reveal());
+    $request = $this->prophesize(Request::class);
+    $query = $this->prophesize(ParameterBag::class);
+    $query->get('filter')->willReturn(['filed1' => 'lorem']);
+    $query->has(Argument::type('string'))->willReturn(FALSE);
+    $query->has('filter')->willReturn(TRUE);
+    $request->query = $query->reveal();
+
+    $route = $this->prophesize(Route::class);
+    $route->getRequirement('_entity_type')->willReturn('dolor');
+    $defaults = $object->enhance([
+      RouteObjectInterface::ROUTE_OBJECT => $route->reveal(),
+    ], $request->reveal());
+    $this->assertInstanceOf(Filter::class, $defaults['_json_api_params']['filter']);
+    $this->assertInstanceOf(OffsetPage::class, $defaults['_json_api_params']['page']);
+    $this->assertTrue(empty($defaults['_json_api_params']['sort']));
+  }
+
+  /**
+   * @covers ::enhance
+   */
+  public function testEnhancePage() {
+    $object = new JsonApiParamEnhancer($this->prophesize(EntityFieldManagerInterface::class)->reveal());
+    $request = $this->prophesize(Request::class);
+    $query = $this->prophesize(ParameterBag::class);
+    $query->get('page')->willReturn(['cursor' => 'lorem']);
+    $query->has(Argument::type('string'))->willReturn(FALSE);
+    $query->has('page')->willReturn(TRUE);
+    $request->query = $query->reveal();
+
+    $defaults = $object->enhance([], $request->reveal());
+    $this->assertInstanceOf(OffsetPage::class, $defaults['_json_api_params']['page']);
+    $this->assertTrue(empty($defaults['_json_api_params']['filter']));
+    $this->assertTrue(empty($defaults['_json_api_params']['sort']));
+  }
+
+  /**
+   * @covers ::enhance
+   */
+  public function testEnhanceSort() {
+    $object = new JsonApiParamEnhancer($this->prophesize(EntityFieldManagerInterface::class)->reveal());
+    $request = $this->prophesize(Request::class);
+    $query = $this->prophesize(ParameterBag::class);
+    $query->get('sort')->willReturn('-lorem');
+    $query->has(Argument::type('string'))->willReturn(FALSE);
+    $query->has('sort')->willReturn(TRUE);
+    $request->query = $query->reveal();
+
+    $defaults = $object->enhance([], $request->reveal());
+    $this->assertInstanceOf(Sort::class, $defaults['_json_api_params']['sort']);
+    $this->assertInstanceOf(OffsetPage::class, $defaults['_json_api_params']['page']);
+    $this->assertTrue(empty($defaults['_json_api_params']['filter']));
+  }
+
+}
diff --git a/drupal/modules/jsonapi/tests/src/Unit/Routing/Param/FilterTest.php b/drupal/modules/jsonapi/tests/src/Unit/Routing/Param/FilterTest.php
new file mode 100644
index 0000000..40d85a3
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Unit/Routing/Param/FilterTest.php
@@ -0,0 +1,349 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Unit\Routing\Param;
+
+use Drupal\jsonapi\Routing\Param\Filter;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\jsonapi\Routing\Param\Filter
+ * @group jsonapi
+ * @group jsonapi_params
+ */
+class FilterTest extends UnitTestCase {
+
+  /**
+   * @covers ::get
+   * @covers ::expand
+   * @covers ::expandItem
+   * @covers ::validateItem
+   * @dataProvider validFiltersDataProvider
+   */
+  public function testValidFilters($original, $expected) {
+    $filter = new Filter($original);
+    $this->assertEquals($expected, $filter->get());
+  }
+
+  /**
+   * @covers ::get
+   * @covers ::expand
+   * @covers ::expandItem
+   * @covers ::validateItem
+   * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
+   * @dataProvider invalidFiltersDataProvider
+   */
+  public function testInvalidFilters($original) {
+    $filter = new Filter($original);
+    $filter->get();
+  }
+
+  /**
+   * Data provider for testValidFilters().
+   */
+  public function validFiltersDataProvider() {
+    return [
+      // Test case:
+      // filter[foo][value]=bar.
+      [
+        ['foo' => ['value' => 'bar']],
+        ['foo' => ['condition' => ['path' => 'foo', 'value' => 'bar', 'operator' => '=']]],
+      ],
+      // Test case:
+      // filter[0][path]=foo
+      // filter[0][value]=bar.
+      [
+        [0 => ['path' => 'foo', 'value' => 'bar']],
+        [0 => ['condition' => ['path' => 'foo', 'value' => 'bar', 'operator' => '=']]],
+      ],
+      // Test case:
+      // filter[foo][value]=bar
+      // filter[foo][operator]=>.
+      [
+        ['foo' => ['value' => 'bar', 'operator' => '>']],
+        ['foo' => ['condition' => ['path' => 'foo', 'value' => 'bar', 'operator' => '>']]],
+      ],
+      // Test case:
+      // filter[0][path]=foo
+      // filter[0][value]=1
+      // filter[0][operator]=>.
+      [
+        [0 => ['path' => 'foo', 'value' => '1', 'operator' => '>']],
+        [0 => ['condition' => ['path' => 'foo', 'value' => '1', 'operator' => '>']]],
+      ],
+      // Test case:
+      // filter[foo][value][]=1
+      // filter[foo][value][]=2
+      // filter[foo][value][]=3
+      // filter[foo][operator]="NOT IN".
+      [
+        ['foo' => ['value' => ['1', '2', '3'], 'operator' => 'NOT IN']],
+        ['foo' => ['condition' => ['path' => 'foo', 'value' => ['1', '2', '3'], 'operator' => 'NOT IN']]],
+      ],
+      // Test case:
+      // filter[foo][value][]=1
+      // filter[foo][value][]=10
+      // filter[foo][operator]=BETWEEN.
+      [
+        ['foo' => ['value' => ['1', '10'], 'operator' => 'BETWEEN']],
+        ['foo' => ['condition' => ['path' => 'foo', 'value' => ['1', '10'], 'operator' => 'BETWEEN']]],
+      ],
+      // Test case:
+      // filter[0][condition][path]=foo
+      // filter[0][condition][value]=1
+      // filter[0][condition][operator]=>.
+      [
+        [0 => ['condition' => ['path' => 'foo', 'value' => '1', 'operator' => '>']]],
+        [0 => ['condition' => ['path' => 'foo', 'value' => '1', 'operator' => '>']]],
+      ],
+      // Test case:
+      // filter[0][path]=foo
+      // filter[0][value][]=bar
+      // filter[0][value][]=baz.
+      [
+        [0 => ['path' => 'foo', 'value' => ['bar', 'baz']]],
+        [0 => ['condition' => ['path' => 'foo', 'value' => ['bar', 'baz'], 'operator' => '=']]],
+      ],
+      // Test case:
+      // filter[0][path]=foo
+      // filter[0][value][]=bar
+      // filter[0][value][]=baz
+      // filter[0][memberOf]=or-group
+      // filter[or-group][group][conjunction]=OR.
+      [
+        [0 => ['path' => 'foo', 'value' => ['bar', 'baz'], 'memberOf' => 'or-group'], 'or-group' => ['group' => ['conjunction' => 'OR']]],
+        [0 => ['condition' => ['path' => 'foo', 'value' => ['bar', 'baz'], 'operator' => '=', 'memberOf' => 'or-group']], 'or-group' => ['group' => ['conjunction' => 'OR']]],
+      ],
+      // Test case:
+      // filter[0][path]=foo
+      // filter[0][value]=bar
+      // filter[1][condition][path]=baz
+      // filter[1][condition][value]=zab
+      // filter[1][condition][operator]=<>.
+      [
+        [
+          0 => ['path' => 'foo', 'value' => 'bar'],
+          1 => ['condition' => ['path' => 'baz', 'value' => 'zab', 'operator' => '<>']],
+        ],
+        [
+          0 => ['condition' => ['path' => 'foo', 'value' => 'bar', 'operator' => '=']],
+          1 => ['condition' => ['path' => 'baz', 'value' => 'zab', 'operator' => '<>']],
+        ],
+      ],
+      // Test case:
+      // filter[zero][path]=foo
+      // filter[zero][value]=bar
+      // filter[one][condition][path]=baz
+      // filter[one][condition][value]=zab
+      // filter[one][condition][operator]=<>.
+      [
+        [
+          'zero' => ['path' => 'foo', 'value' => 'bar'],
+          'one' => ['condition' => ['path' => 'baz', 'value' => 'zab', 'operator' => '<>']],
+        ],
+        [
+          'zero' => ['condition' => ['path' => 'foo', 'value' => 'bar', 'operator' => '=']],
+          'one' => ['condition' => ['path' => 'baz', 'value' => 'zab', 'operator' => '<>']],
+        ],
+      ],
+      // Test case:
+      // filter[and-group][group][conjunction]=AND
+      // filter[or-group][group][conjunction]=OR
+      // filter[or-group][group][memberOf]=and-group
+      // filter[admin-filter][path]=uid.name
+      // filter[admin-filter][value]=admin
+      // filter[admin-filter][memberOf]=and-group
+      // filter[sticky-filter][path]=sticky
+      // filter[sticky-filter][value]=1
+      // filter[sticky-filter][memberOf]=or-group
+      // filter[promote-filter][path]=promote
+      // filter[promote-filter][value]=1
+      // filter[promote-filter][memberOf]=or-group.
+      [
+        [
+          'and-group' => ['group' => ['conjunction' => 'AND']],
+          'or-group' => ['group' => ['conjunction' => 'OR', 'memberOf' => 'and-group']],
+          'admin-filter' => ['path' => 'uid.name', 'value' => 'admin', 'memberOf' => 'and-group'],
+          'sticky-filter' => ['path' => 'sticky', 'value' => 1, 'memberOf' => 'or-group'],
+          'promote-filter' => ['path' => 'promote', 'value' => 1, 'memberOf' => 'or-group'],
+        ],
+        [
+          'and-group' => ['group' => ['conjunction' => 'AND']],
+          'or-group' => ['group' => ['conjunction' => 'OR', 'memberOf' => 'and-group']],
+          'admin-filter' => ['condition' => ['path' => 'uid.name', 'value' => 'admin', 'operator' => '=', 'memberOf' => 'and-group']],
+          'sticky-filter' => ['condition' => ['path' => 'sticky', 'value' => 1, 'operator' => '=', 'memberOf' => 'or-group']],
+          'promote-filter' => ['condition' => ['path' => 'promote', 'value' => 1, 'operator' => '=', 'memberOf' => 'or-group']],
+        ],
+      ],
+      // Test case:
+      // filter[and-group][group][conjunction]=AND
+      // filter[or-group][group][conjunction]=OR
+      // filter[or-group][group][memberOf]=and-group
+      // filter[admin-filter][condition][path]=uid.name
+      // filter[admin-filter][condition][value]=admin
+      // filter[admin-filter][condition][memberOf]=and-group
+      // filter[sticky-filter][condition][path]=sticky
+      // filter[sticky-filter][condition][value]=1
+      // filter[sticky-filter][condition][memberOf]=or-group
+      // filter[promote-filter][condition][path]=promote
+      // filter[promote-filter][condition][value]=1
+      // filter[promote-filter][condition][memberOf]=or-group.
+      [
+        [
+          'and-group' => ['group' => ['conjunction' => 'AND']],
+          'or-group' => ['group' => ['conjunction' => 'OR', 'memberOf' => 'and-group']],
+          'admin-filter' => ['condition' => ['path' => 'uid.name', 'value' => 'admin', 'memberOf' => 'and-group']],
+          'sticky-filter' => ['condition' => ['path' => 'sticky', 'value' => 1, 'memberOf' => 'or-group']],
+          'promote-filter' => ['condition' => ['path' => 'promote', 'value' => 1, 'memberOf' => 'or-group']],
+        ],
+        [
+          'and-group' => ['group' => ['conjunction' => 'AND']],
+          'or-group' => ['group' => ['conjunction' => 'OR', 'memberOf' => 'and-group']],
+          'admin-filter' => ['condition' => ['path' => 'uid.name', 'value' => 'admin', 'memberOf' => 'and-group']],
+          'sticky-filter' => ['condition' => ['path' => 'sticky', 'value' => 1, 'memberOf' => 'or-group']],
+          'promote-filter' => ['condition' => ['path' => 'promote', 'value' => 1, 'memberOf' => 'or-group']],
+        ],
+      ],
+
+      // filter[has-sticky][path]=sticky
+      // filter[has-sticky[operator]='IS NOT NULL'
+      [
+        ['has-sticky' => ['condition' => ['path' => 'sticky', 'operator' => 'IS NOT NULL']]],
+        ['has-sticky' => [
+          'condition' => ['path' => 'sticky', 'operator' => 'IS NOT NULL', 'value' => NULL]
+        ]],
+      ]
+    ];
+  }
+
+  /**
+   * Data provider for testInvalidFilters().
+   */
+  public function invalidFiltersDataProvider() {
+    return [
+      // Filter suppors only arrays.
+      // # Test case:
+      // filter[foo]=bar
+      // # Reason to fail:
+      // "bar" is a string and not an array.
+      [
+        ['foo' => 'bar'],
+      ],
+      // Filter supports only certain keys.
+      // # Test case:
+      // filter[foo][nid]=1
+      // # Reason to fail:
+      // Filter expects "group", "condition" or "value" key in the filter root.
+      [
+       ['foo' => ['nid' => 1]],
+      ],
+      // Shorthand filter supports only allowed list of params.
+      // # Test case:
+      // filter[foo][value]=1
+      // filter[foo][bar]=baz
+      // # Reason to fail:
+      // "bar" is not expected key for filtering.
+      [
+        ['foo' => ['value' => 1, 'bar' => 'baz']],
+      ],
+      // Shorthand filter supports only allowed list of params.
+      // # Test case:
+      // filter[foo][value]=1
+      // filter[foo][group]=bar
+      // # Reason to fail:
+      // "group" is a legacy and not supported anymore group key.
+      [
+        ['foo' => ['value' => 1, 'group' => 'bar']],
+      ],
+      // Full canonical filter has mandatory params.
+      // # Test case:
+      // filter[foo][condition][value]=1
+      // # Reason to fail:
+      // Missing mandatory "path" key.
+      [
+        ['foo' => ['condition' => ['value' => 1]]],
+      ],
+      // Full canonical filter has mandatory params.
+      // # Test case:
+      // filter[foo][condition][path]=nid
+      // # Reason to fail:
+      // Missing mandatory "value" key.
+      [
+        ['foo' => ['condition' => ['path' => 'nid']]],
+      ],
+      // Full canonical filter supports only allowed list of params.
+      // # Test case:
+      // filter[foo][condition][value]=1
+      // filter[foo][condition][path]=nid
+      // filter[foo][condition][bar]=baz.
+      // # Reason to fail:
+      // "bar" is not expected filtering key.
+      [
+        ['foo' => ['condition' => ['value' => 1, 'path' => 'nid', 'bar' => 'baz']]],
+      ],
+      // Full canonical filter allows only one top level key "condition".
+      // # Test case:
+      // filter[foo][condition][value]=1
+      // filter[foo][condition][path]=nid.
+      // filter[foo][value]=baz.
+      // # Reason to fail:
+      // "value" => "bar" is not expected next to the filter[condition] query.
+      [
+        ['foo' => ['condition' => ['value' => 1, 'path' => 'nid'], ['value' => 'baz']]],
+      ],
+      // Group query supports only allowed list of params.
+      // # Test case:
+      // filter[foo][group][conjunction]=AND
+      // filter[foo][group][bar]=baz
+      // # Reason to fail:
+      // "bar" is not supported group key.
+      [
+        ['foo' => ['group' => ['conjunction' => 'AND', 'bar' => 'baz']]],
+      ],
+      // Group query supports only allowed list of params.
+      // # Test case:
+      // filter[foo][group][conjunction]=AND
+      // filter[foo][group][group]=bar
+      // # Reason to fail:
+      // "group" is a legacy and not supported anymore group key.
+      [
+        ['foo' => ['group' => ['conjunction' => 'AND', 'group' => 'bar']]],
+      ],
+      // Group query supports only allowed list of params.
+      // # Test case:
+      // filter[foo][group][bar]=baz
+      // filter[foo][group][conjunction]=AND
+      // # Reason to fail:
+      // "bar" is not supported group key.
+      [
+        ['foo' => ['group' => ['bar' => 'baz', 'conjunction' => 'AND']]],
+      ],
+      // Group query has mandatory field "conjunction".
+      // # Test case:
+      // filter[foo][group][memberOf]=bar
+      // # Reason to fail:
+      // "conjunction" key is missing.
+      [
+        ['foo' => ['group' => ['memberOf' => 'bar']]],
+      ],
+      // Group query has only certain correct values for "conjunction" key.
+      // # Test case:
+      // filter[foo][group][conjunction]=NOR
+      // # Reason to fail:
+      // "conjunction" key has wrong value.
+      [
+        ['foo' => ['group' => ['conjunction' => 'NOR']]],
+      ],
+      // Group query allows only one top level key "group".
+      // # Test case:
+      // filter[foo][group][conjunction]=AND
+      // filter[foo][group][memberOf]=bar
+      // filter[foo][value]=baz.
+      // # Reason to fail:
+      // "value" => "bar" is not expected next to the filter[condition] query.
+      [
+        ['foo' => ['group' => ['conjunction' => 'AND', 'memberOf' => 'bar'], ['value' => 'baz']]],
+      ],
+    ];
+  }
+
+}
diff --git a/drupal/modules/jsonapi/tests/src/Unit/Routing/Param/OffsetPageTest.php b/drupal/modules/jsonapi/tests/src/Unit/Routing/Param/OffsetPageTest.php
new file mode 100644
index 0000000..c4afea5
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Unit/Routing/Param/OffsetPageTest.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Unit\Routing\Param;
+
+use Drupal\jsonapi\Routing\Param\OffsetPage;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\jsonapi\Routing\Param\OffsetPage
+ * @group jsonapi
+ */
+class OffsetPageTest extends UnitTestCase {
+
+  /**
+   * @covers ::get
+   * @dataProvider getProvider
+   */
+  public function testGet($original, $max_page, $expected) {
+    $pager = new OffsetPage($original, $max_page);
+    $this->assertEquals($expected, $pager->get());
+  }
+
+  /**
+   * Data provider for testGet.
+   */
+  public function getProvider() {
+    return [
+      [['offset' => 12, 'limit' => 20], 50, ['offset' => 12, 'limit' => 20]],
+      [['offset' => 12, 'limit' => 60], 50, ['offset' => 12, 'limit' => 50]],
+      [['offset' => 12], 50, ['offset' => 12, 'limit' => 50]],
+      [['offset' => 0], 50, ['offset' => 0, 'limit' => 50]],
+      [[], 50, ['limit' => 50]],
+    ];
+  }
+
+  /**
+   * @covers ::get
+   * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
+   */
+  public function testGetFail() {
+    $pager = new OffsetPage('lorem');
+    $pager->get();
+  }
+
+}
diff --git a/drupal/modules/jsonapi/tests/src/Unit/Routing/Param/SortTest.php b/drupal/modules/jsonapi/tests/src/Unit/Routing/Param/SortTest.php
new file mode 100644
index 0000000..845811d
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Unit/Routing/Param/SortTest.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Unit\Routing\Param;
+
+use Drupal\jsonapi\Routing\Param\Sort;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\jsonapi\Routing\Param\Sort
+ * @group jsonapi
+ */
+class SortTest extends UnitTestCase {
+
+  /**
+   * @covers ::get
+   * @dataProvider getProvider
+   */
+  public function testGet($original, $expected) {
+    $sort = new Sort($original);
+    $this->assertEquals($expected, $sort->get());
+  }
+
+  /**
+   * Data provider for testGet.
+   */
+  public function getProvider() {
+    return [
+      ['lorem', [['path' => 'lorem', 'direction' => 'ASC', 'langcode' => NULL]]],
+      ['-lorem', [['path' => 'lorem', 'direction' => 'DESC', 'langcode' => NULL]]],
+      ['-lorem,ipsum', [
+        ['path' => 'lorem', 'direction' => 'DESC', 'langcode' => NULL],
+        ['path' => 'ipsum', 'direction' => 'ASC', 'langcode' => NULL],
+      ],
+      ],
+      ['-lorem,-ipsum', [
+        ['path' => 'lorem', 'direction' => 'DESC', 'langcode' => NULL],
+        ['path' => 'ipsum', 'direction' => 'DESC', 'langcode' => NULL],
+      ],
+      ],
+      [[
+        ['path' => 'lorem', 'langcode' => NULL],
+        ['path' => 'ipsum', 'langcode' => 'ca'],
+        ['path' => 'dolor', 'direction' => 'ASC', 'langcode' => 'ca'],
+        ['path' => 'sit', 'direction' => 'DESC', 'langcode' => 'ca'],
+      ], [
+        ['path' => 'lorem', 'direction' => 'ASC', 'langcode' => NULL],
+        ['path' => 'ipsum', 'direction' => 'ASC', 'langcode' => 'ca'],
+        ['path' => 'dolor', 'direction' => 'ASC', 'langcode' => 'ca'],
+        ['path' => 'sit', 'direction' => 'DESC', 'langcode' => 'ca'],
+      ],
+      ],
+    ];
+  }
+
+  /**
+   * @covers ::get
+   * @dataProvider getFailProvider
+   * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
+   */
+  public function testGetFail($input) {
+    $sort = new Sort($input);
+    $sort->get();
+  }
+
+  /**
+   * Data provider for testGetFail.
+   */
+  public function getFailProvider() {
+    return [
+      [[['lorem']]],
+      [''],
+    ];
+  }
+
+}
diff --git a/drupal/modules/jsonapi/tests/src/Unit/Routing/RoutesTest.php b/drupal/modules/jsonapi/tests/src/Unit/Routing/RoutesTest.php
new file mode 100644
index 0000000..5159919
--- /dev/null
+++ b/drupal/modules/jsonapi/tests/src/Unit/Routing/RoutesTest.php
@@ -0,0 +1,128 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Unit\Routing;
+
+use Drupal\Core\Authentication\AuthenticationCollectorInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\jsonapi\ResourceType\ResourceType;
+use Drupal\jsonapi\ResourceType\ResourceTypeRepository;
+use Drupal\jsonapi\Routing\Routes;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * @coversDefaultClass \Drupal\jsonapi\Routing\Routes
+ * @group jsonapi
+ */
+class RoutesTest extends UnitTestCase {
+
+  /**
+   * List of routes objects for the different scenarios.
+   *
+   * @var \Drupal\jsonapi\Routing\Routes[]
+   */
+  protected $routes;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $resource_type_repository = $this->prophesize(ResourceTypeRepository::class);
+    $resource_type_repository->all()->willReturn([new ResourceType('entity_type_1', 'bundle_1_1', EntityInterface::class)]);
+    $container = $this->prophesize(ContainerInterface::class);
+    $container->get('jsonapi.resource_type.repository')->willReturn($resource_type_repository->reveal());
+    $auth_collector = $this->prophesize(AuthenticationCollectorInterface::class);
+    $auth_collector->getSortedProviders()->willReturn([
+      'lorem' => [],
+      'ipsum' => [],
+    ]);
+    $container->get('authentication_collector')->willReturn($auth_collector->reveal());
+
+    $this->routes['ok'] = Routes::create($container->reveal());
+  }
+
+  /**
+   * @covers ::routes
+   */
+  public function testRoutesCollection() {
+    // Get the route collection and start making assertions.
+    $routes = $this->routes['ok']->routes();
+
+    // Make sure that there are 4 routes for each resource.
+    $this->assertEquals(4, $routes->count());
+
+    $iterator = $routes->getIterator();
+    // Check the collection route.
+    /** @var \Symfony\Component\Routing\Route $route */
+    $route = $iterator->offsetGet('jsonapi.entity_type_1--bundle_1_1.collection');
+    $this->assertSame('/jsonapi/entity_type_1/bundle_1_1', $route->getPath());
+    $this->assertSame('entity_type_1', $route->getRequirement('_entity_type'));
+    $this->assertSame('bundle_1_1', $route->getRequirement('_bundle'));
+    $this->assertSame(['lorem', 'ipsum'], $route->getOption('_auth'));
+    $this->assertEquals(['GET', 'POST'], $route->getMethods());
+    $this->assertSame(Routes::FRONT_CONTROLLER, $route->getDefault(RouteObjectInterface::CONTROLLER_NAME));
+    $this->assertSame('Drupal\jsonapi\Resource\JsonApiDocumentTopLevel', $route->getOption('serialization_class'));
+  }
+
+  /**
+   * @covers ::routes
+   */
+  public function testRoutesIndividual() {
+    // Get the route collection and start making assertions.
+    $iterator = $this->routes['ok']->routes()->getIterator();
+
+    // Check the individual route.
+    /** @var \Symfony\Component\Routing\Route $route */
+    $route = $iterator->offsetGet('jsonapi.entity_type_1--bundle_1_1.individual');
+    $this->assertSame('/jsonapi/entity_type_1/bundle_1_1/{entity_type_1}', $route->getPath());
+    $this->assertSame('entity_type_1', $route->getRequirement('_entity_type'));
+    $this->assertSame('bundle_1_1', $route->getRequirement('_bundle'));
+    $this->assertEquals(['GET', 'PATCH', 'DELETE'], $route->getMethods());
+    $this->assertSame(Routes::FRONT_CONTROLLER, $route->getDefault(RouteObjectInterface::CONTROLLER_NAME));
+    $this->assertSame('Drupal\jsonapi\Resource\JsonApiDocumentTopLevel', $route->getOption('serialization_class'));
+    $this->assertSame(['lorem', 'ipsum'], $route->getOption('_auth'));
+    $this->assertEquals(['entity_type_1' => ['type' => 'entity:entity_type_1']], $route->getOption('parameters'));
+  }
+
+  /**
+   * @covers ::routes
+   */
+  public function testRoutesRelated() {
+    // Get the route collection and start making assertions.
+    $iterator = $this->routes['ok']->routes()->getIterator();
+
+    // Check the related route.
+    /** @var \Symfony\Component\Routing\Route $route */
+    $route = $iterator->offsetGet('jsonapi.entity_type_1--bundle_1_1.related');
+    $this->assertSame('/jsonapi/entity_type_1/bundle_1_1/{entity_type_1}/{related}', $route->getPath());
+    $this->assertSame('entity_type_1', $route->getRequirement('_entity_type'));
+    $this->assertSame('bundle_1_1', $route->getRequirement('_bundle'));
+    $this->assertEquals(['GET'], $route->getMethods());
+    $this->assertSame(Routes::FRONT_CONTROLLER, $route->getDefault(RouteObjectInterface::CONTROLLER_NAME));
+    $this->assertSame(['lorem', 'ipsum'], $route->getOption('_auth'));
+    $this->assertEquals(['entity_type_1' => ['type' => 'entity:entity_type_1']], $route->getOption('parameters'));
+  }
+
+  /**
+   * @covers ::routes
+   */
+  public function testRoutesRelationships() {
+    // Get the route collection and start making assertions.
+    $iterator = $this->routes['ok']->routes()->getIterator();
+
+    // Check the relationships route.
+    /** @var \Symfony\Component\Routing\Route $route */
+    $route = $iterator->offsetGet('jsonapi.entity_type_1--bundle_1_1.relationship');
+    $this->assertSame('/jsonapi/entity_type_1/bundle_1_1/{entity_type_1}/relationships/{related}', $route->getPath());
+    $this->assertSame('entity_type_1', $route->getRequirement('_entity_type'));
+    $this->assertSame('bundle_1_1', $route->getRequirement('_bundle'));
+    $this->assertEquals(['GET', 'POST', 'PATCH', 'DELETE'], $route->getMethods());
+    $this->assertSame(Routes::FRONT_CONTROLLER, $route->getDefault(RouteObjectInterface::CONTROLLER_NAME));
+    $this->assertSame(['lorem', 'ipsum'], $route->getOption('_auth'));
+    $this->assertEquals(['entity_type_1' => ['type' => 'entity:entity_type_1']], $route->getOption('parameters'));
+    $this->assertSame('Drupal\Core\Field\EntityReferenceFieldItemList', $route->getOption('serialization_class'));
+  }
+
+}
diff --git a/drupal/modules/jsonb/LICENSE.txt b/drupal/modules/jsonb/LICENSE.txt
new file mode 100755
index 0000000..d159169
--- /dev/null
+++ b/drupal/modules/jsonb/LICENSE.txt
@@ -0,0 +1,339 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/drupal/modules/jsonb/config/schema/jsonb.schema.yml b/drupal/modules/jsonb/config/schema/jsonb.schema.yml
new file mode 100755
index 0000000..8b84e9c
--- /dev/null
+++ b/drupal/modules/jsonb/config/schema/jsonb.schema.yml
@@ -0,0 +1,9 @@
+# Schema for the configuration files of the text module.
+
+field.value.jsonb:
+  type: mapping
+  label: 'Default value'
+  mapping:
+    value:
+      type: text
+      label: 'Value'
\ No newline at end of file
diff --git a/drupal/modules/jsonb/jsonb.info.yml b/drupal/modules/jsonb/jsonb.info.yml
new file mode 100755
index 0000000..5d84811
--- /dev/null
+++ b/drupal/modules/jsonb/jsonb.info.yml
@@ -0,0 +1,11 @@
+name: JSON/JSONB Field
+type: module
+description: Creates field types that use the JSON/JSONB data type to store JSON object data.
+# core: 8.x
+dependencies:
+  - field
+# Information added by Drupal.org packaging script on 2017-05-22
+version: '8.x-1.0-beta2'
+core: '8.x'
+project: 'jsonb'
+datestamp: 1495467487
diff --git a/drupal/modules/jsonb/jsonb.install b/drupal/modules/jsonb/jsonb.install
new file mode 100755
index 0000000..9a6b37c
--- /dev/null
+++ b/drupal/modules/jsonb/jsonb.install
@@ -0,0 +1,13 @@
+<?php
+
+use Drupal\Core\Database\Database;
+
+/**
+ * Implements hook_requirements().
+ * @param $phase
+ * @return array
+ */
+function jsonb_requirements($phase) {
+  $requirements = [];
+  return $requirements;
+}
\ No newline at end of file
diff --git a/drupal/modules/jsonb/jsonb.module b/drupal/modules/jsonb/jsonb.module
new file mode 100755
index 0000000..a4abe2d
--- /dev/null
+++ b/drupal/modules/jsonb/jsonb.module
@@ -0,0 +1,2 @@
+<?php
+
diff --git a/drupal/modules/jsonb/src/Plugin/Field/FieldFormatter/JsonbDefaultFormatter.php b/drupal/modules/jsonb/src/Plugin/Field/FieldFormatter/JsonbDefaultFormatter.php
new file mode 100755
index 0000000..a530df9
--- /dev/null
+++ b/drupal/modules/jsonb/src/Plugin/Field/FieldFormatter/JsonbDefaultFormatter.php
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\jsonb\Plugin\field\formatter\JsonbDefaultFormatter.
+ */
+
+namespace Drupal\jsonb\Plugin\Field\FieldFormatter;
+
+use Drupal\Core\Field\FormatterBase;
+use Drupal\Core\Field\FieldItemListInterface;
+
+/**
+ * Plugin implementation of the 'jsonb_default' formatter.
+ *
+ * @FieldFormatter(
+ *   id = "jsonb_default",
+ *   label = @Translation("Default"),
+ *   field_types = {
+ *     "jsonb",
+ *     "json",
+ *   }
+ * )
+ */
+class JsonbDefaultFormatter extends FormatterBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function viewElements(FieldItemListInterface $items, $langcode) {
+    $elements = array();
+
+    foreach ($items as $delta => $item) {
+      $elements[$delta] = array(
+        '#type' => 'processed_text',
+        '#text' => $item->value,
+        '#format' => 'plain_text',
+        '#langcode' => $item->getLangcode(),
+      );
+    }
+
+    return $elements;
+  }
+}
diff --git a/drupal/modules/jsonb/src/Plugin/Field/FieldType/JsonItem.php b/drupal/modules/jsonb/src/Plugin/Field/FieldType/JsonItem.php
new file mode 100755
index 0000000..2ac37c6
--- /dev/null
+++ b/drupal/modules/jsonb/src/Plugin/Field/FieldType/JsonItem.php
@@ -0,0 +1,60 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\jsonb\Plugin\Field\FieldType\JsonItem.
+ */
+
+namespace Drupal\jsonb\Plugin\Field\FieldType;
+
+use Drupal\Core\Field\FieldItemBase;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\TypedData\DataDefinition;
+
+/**
+ * Plugin implementation of the 'json' field type.
+ *
+ * @FieldType(
+ *   id = "json",
+ *   label = @Translation("JSON"),
+ *   description = @Translation("This field stores a JSON object or an array of JSON objects."),
+ *   category = @Translation("Document"),
+ *   default_widget = "jsonb_textarea",
+ *   default_formatter = "jsonb_default"
+ * )
+ */
+class JsonItem extends FieldItemBase {
+  /**
+   * {@inheritdoc}
+   */
+  public static function schema(FieldStorageDefinitionInterface $field_definition) {
+    return array(
+      'columns' => array(
+        'value' => array(
+          'type' => 'json',
+          'pgsql_type' => 'json',
+          'mysql_type' => 'json',
+          'not null' => FALSE,
+        ),
+      ),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isEmpty() {
+    $value = $this->get('value')->getValue();
+    return $value === NULL || $value === '';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
+    $properties['value'] = DataDefinition::create('string')
+      ->setLabel(t('JSON value'));
+
+    return $properties;
+  }
+}
diff --git a/drupal/modules/jsonb/src/Plugin/Field/FieldType/JsonbItem.php b/drupal/modules/jsonb/src/Plugin/Field/FieldType/JsonbItem.php
new file mode 100755
index 0000000..072741d
--- /dev/null
+++ b/drupal/modules/jsonb/src/Plugin/Field/FieldType/JsonbItem.php
@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\jsonb\Plugin\Field\FieldType\JsonbItem.
+ */
+
+namespace Drupal\jsonb\Plugin\Field\FieldType;
+
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\TypedData\DataDefinition;
+
+/**
+ * Plugin implementation of the 'jsonb' field type.
+ *
+ * @FieldType(
+ *   id = "jsonb",
+ *   label = @Translation("JSONB"),
+ *   description = @Translation("This field stores a JSON object or an array of JSON objects."),
+ *   category = @Translation("Document"),
+ *   default_widget = "jsonb_textarea",
+ *   default_formatter = "jsonb_default"
+ * )
+ */
+class JsonbItem extends JsonItem {
+  /**
+   * {@inheritdoc}
+   */
+  public static function schema(FieldStorageDefinitionInterface $field_definition) {
+    return array(
+      'columns' => array(
+        'value' => array(
+          'type' => 'text',
+          'pgsql_type' => 'jsonb',
+          'mysql_type' => 'json',
+          'not null' => FALSE,
+        ),
+      ),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
+    $properties['value'] = DataDefinition::create('string')
+      ->setLabel(t('JSONB value'));
+
+    return $properties;
+  }
+}
diff --git a/drupal/modules/jsonb/src/Plugin/Field/FieldWidget/JsonbWidget.php b/drupal/modules/jsonb/src/Plugin/Field/FieldWidget/JsonbWidget.php
new file mode 100755
index 0000000..ac7acfb
--- /dev/null
+++ b/drupal/modules/jsonb/src/Plugin/Field/FieldWidget/JsonbWidget.php
@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\jsonb\Plugin\Field\FieldWidget\JsonbWidget.
+ */
+
+namespace Drupal\jsonb\Plugin\Field\FieldWidget;
+
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Field\Plugin\Field\FieldWidget\StringTextareaWidget;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Component\Utility\Unicode as Unicode;
+use Drupal\Component\Serialization\Json as Json;
+
+/**
+ * Plugin implementation of the 'jsonb_textarea' widget.
+ *
+ * @FieldWidget(
+ *   id = "jsonb_textarea",
+ *   label = @Translation("JSONB Object"),
+ *   field_types = {
+ *     "jsonb",
+ *     "json",
+ *   }
+ * )
+ */
+class JsonbWidget extends StringTextareaWidget {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
+    $widget = parent::formElement($items, $delta, $element, $form, $form_state);
+    $widget['#element_validate'][] = array(get_called_class(), 'validateJsonStructure');
+    return $widget;
+  }
+
+  /**
+   * Validates the input to see if it is a properly formatted JSON object. If not, PgSQL will throw fatal errors upon insert.
+   *
+   * @param $element
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   * @param $form
+   */
+  public static function validateJsonStructure(&$element, FormStateInterface $form_state, $form) {
+    if (Unicode::strlen($element['value']['#value'])) {
+      $value = Json::decode($element['value']['#value']);
+
+      if (json_last_error() !== JSON_ERROR_NONE) {
+        $form_state->setError($element['value'], t('!name must contain a valid JSON object.', array('!name' => $element['value']['#title'])));
+      }
+    }
+  }
+}
diff --git a/drupal/modules/restui/.gitignore b/drupal/modules/restui/.gitignore
new file mode 100755
index 0000000..723ef36
--- /dev/null
+++ b/drupal/modules/restui/.gitignore
@@ -0,0 +1 @@
+.idea
\ No newline at end of file
diff --git a/drupal/modules/restui/LICENSE.txt b/drupal/modules/restui/LICENSE.txt
new file mode 100755
index 0000000..d159169
--- /dev/null
+++ b/drupal/modules/restui/LICENSE.txt
@@ -0,0 +1,339 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/drupal/modules/restui/README.txt b/drupal/modules/restui/README.txt
new file mode 100755
index 0000000..1b10a99
--- /dev/null
+++ b/drupal/modules/restui/README.txt
@@ -0,0 +1,11 @@
+REST UI
+=======
+
+This module provides a user interface to manage REST resources.
+
+Installation
+============
+
+Once the module has been installed, navigate to admin/config/services/rest
+(Configuration > Web Services > REST through the administration panel) and
+configure the available resources.
diff --git a/drupal/modules/restui/restui.info.yml b/drupal/modules/restui/restui.info.yml
new file mode 100755
index 0000000..cc53379
--- /dev/null
+++ b/drupal/modules/restui/restui.info.yml
@@ -0,0 +1,15 @@
+name: REST UI
+type: module
+description: "Provides a user interface to manage REST resources"
+package: Web services
+# core: 8.x
+configure: restui.list
+dependencies:
+  - rest
+  - system (>=8.2.0)
+
+# Information added by Drupal.org packaging script on 2017-06-08
+version: '8.x-1.15'
+core: '8.x'
+project: 'restui'
+datestamp: 1496919844
diff --git a/drupal/modules/restui/restui.links.menu.yml b/drupal/modules/restui/restui.links.menu.yml
new file mode 100755
index 0000000..8537224
--- /dev/null
+++ b/drupal/modules/restui/restui.links.menu.yml
@@ -0,0 +1,5 @@
+restui.list:
+  title: REST
+  description: 'Configure REST server settings.'
+  parent: system.admin_config_services
+  route_name: restui.list
diff --git a/drupal/modules/restui/restui.module b/drupal/modules/restui/restui.module
new file mode 100755
index 0000000..0c90041
--- /dev/null
+++ b/drupal/modules/restui/restui.module
@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * @file
+ * Hook implementations for REST UI module.
+ */
+
+use Drupal\Core\Routing\RouteMatchInterface;
+
+/**
+ * Implements hook_help().
+ */
+function restui_help($route_name, RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    case 'restui.edit':
+      return '<p>' . t('Here you can restrict which HTTP methods should this resource support. And within each method, the available serialization formats and authentication providers.') . '</p>';
+  }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function restui_theme() {
+  return [
+    // List resources.
+    'restui_resource_info' => [
+      'variables' => [
+        'granularity' => [],
+        'configuration' => [],
+      ],
+      'template' => 'restui-resource-info',
+    ],
+  ];
+}
diff --git a/drupal/modules/restui/restui.post_update.php b/drupal/modules/restui/restui.post_update.php
new file mode 100755
index 0000000..591e1c2
--- /dev/null
+++ b/drupal/modules/restui/restui.post_update.php
@@ -0,0 +1,20 @@
+<?php
+
+/**
+ * @file
+ * Post update functions for REST UI.
+ */
+
+/**
+ * Simplify method-granularity REST resource config to resource-granularity.
+ *
+ * Re-runs the REST module's update path, because the REST UI module only
+ * allowed creating 'method' granularity resources until version 1.14.
+ *
+ * @see https://www.drupal.org/node/2869443
+ * @see https://www.drupal.org/node/2721595
+ */
+function restui_post_update_resource_granularity() {
+  require_once \Drupal::root() . '/core/modules/rest/rest.post_update.php';
+  rest_post_update_resource_granularity();
+}
diff --git a/drupal/modules/restui/restui.routing.yml b/drupal/modules/restui/restui.routing.yml
new file mode 100755
index 0000000..3a0c33f
--- /dev/null
+++ b/drupal/modules/restui/restui.routing.yml
@@ -0,0 +1,21 @@
+restui.list:
+  path: '/admin/config/services/rest'
+  defaults:
+    _controller: '\Drupal\restui\Controller\RestUIController::listResources'
+  requirements:
+    _permission: 'administer rest resources'
+
+restui.disable:
+  path: '/admin/config/services/rest/resource/{resource_id}/disable'
+  defaults:
+    _controller: '\Drupal\restui\Controller\RestUIController::disable'
+  requirements:
+    _permission: 'administer rest resources'
+    _csrf_token: 'TRUE'
+
+restui.edit:
+  path: '/admin/config/services/rest/resource/{resource_id}/edit'
+  defaults:
+    _form: '\Drupal\restui\Form\RestUIForm'
+  requirements:
+    _permission: 'administer rest resources'
diff --git a/drupal/modules/restui/src/Controller/RestUIController.php b/drupal/modules/restui/src/Controller/RestUIController.php
new file mode 100755
index 0000000..c85d5a3
--- /dev/null
+++ b/drupal/modules/restui/src/Controller/RestUIController.php
@@ -0,0 +1,300 @@
+<?php
+
+namespace Drupal\restui\Controller;
+
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
+use Drupal\rest\Plugin\Type\ResourcePluginManager;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Url;
+
+/**
+ * Controller routines for REST resources.
+ */
+class RestUIController implements ContainerInjectionInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * Resource plugin manager.
+   *
+   * @var \Drupal\rest\Plugin\Type\ResourcePluginManager
+   */
+  protected $resourcePluginManager;
+
+  /**
+   * The URL generator to use.
+   *
+   * @var \Symfony\Component\Routing\Generator\UrlGeneratorInterface
+   */
+  protected $urlGenerator;
+
+  /**
+   * Configuration entity to store enabled REST resources.
+   *
+   * @var \Drupal\rest\RestResourceConfigInterface
+   */
+  protected $resourceConfigStorage;
+
+  /**
+   * Injects RestUIManager Service.
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('plugin.manager.rest'),
+      $container->get('url_generator'),
+      $container->get('entity_type.manager')->getStorage('rest_resource_config')
+    );
+  }
+
+  /**
+   * Constructs a RestUIController object.
+   *
+   * @param \Drupal\rest\Plugin\Type\ResourcePluginManager $resourcePluginManager
+   *   The REST resource plugin manager.
+   * @param \Symfony\Component\Routing\Generator\UrlGeneratorInterface $url_generator
+   *   The URL generator.
+   * @param \Drupal\Core\Entity\EntityStorageInterface $resource_config_storage
+   *   The REST resource config storage.
+   */
+  public function __construct(ResourcePluginManager $resourcePluginManager, UrlGeneratorInterface $url_generator, EntityStorageInterface $resource_config_storage) {
+    $this->resourcePluginManager = $resourcePluginManager;
+    $this->urlGenerator = $url_generator;
+    $this->resourceConfigStorage = $resource_config_storage;
+  }
+
+  /**
+   * Returns an administrative overview of all REST resources.
+   *
+   * @return string
+   *   A HTML-formatted string with the administrative page content.
+   */
+  public function listResources() {
+    // Get the list of enabled and disabled resources.
+    $config = $this->resourceConfigStorage->loadMultiple();
+
+    // Strip out the nested method configuration, we are only interested in the
+    // plugin IDs of the resources.
+    $enabled_resources = array_combine(array_keys($config), array_keys($config));
+    $available_resources = ['enabled' => [], 'disabled' => []];
+    $resources = $this->resourcePluginManager->getDefinitions();
+    foreach ($resources as $id => $resource) {
+      $key = $this->getResourceKey($id);
+      $status = (in_array($key, $enabled_resources) && $config[$key]->status()) ? 'enabled' : 'disabled';
+      $available_resources[$status][$id] = $resource;
+    }
+
+    // Sort the list of resources by label.
+    $sort_resources = function ($resource_a, $resource_b) {
+      return strcmp($resource_a['label'], $resource_b['label']);
+    };
+    if (!empty($available_resources['enabled'])) {
+      uasort($available_resources['enabled'], $sort_resources);
+    }
+    if (!empty($available_resources['disabled'])) {
+      uasort($available_resources['disabled'], $sort_resources);
+    }
+
+    // Heading.
+    $list['resources_title'] = [
+      '#markup' => '<h2>' . $this->t('REST resources') . '</h2>',
+    ];
+    $list['resources_help'] = [
+      '#markup' => '<p>' . $this->t('Here you can enable and disable available resources. Once a resource has been enabled, you can restrict its formats and authentication by clicking on its "Edit" link.') . '</p>',
+    ];
+    $list['enabled']['heading']['#markup'] = '<h2>' . $this->t('Enabled') . '</h2>';
+    $list['disabled']['heading']['#markup'] = '<h2>' . $this->t('Disabled') . '</h2>';
+
+    // List of resources.
+    foreach (['enabled', 'disabled'] as $status) {
+      $list[$status]['#type'] = 'container';
+      $list[$status]['#attributes'] = ['class' => ['rest-ui-list-section', $status]];
+      $list[$status]['table'] = [
+        '#theme' => 'table',
+        '#header' => [
+          'resource_name' => [
+            'data' => $this->t('Resource name'),
+            'class' => ['rest-ui-name']
+          ],
+          'path' => [
+            'data' => $this->t('Path'),
+            'class' => ['views-ui-path'],
+          ],
+          'description' => [
+            'data' => $this->t('Description'),
+            'class' => ['rest-ui-description'],
+          ],
+          'operations' => [
+            'data' => $this->t('Operations'),
+            'class' => ['rest-ui-operations'],
+          ],
+        ],
+        '#rows' => [],
+      ];
+      foreach ($available_resources[$status] as $id => $resource) {
+        $canonical_uri_path = !empty($resource['uri_paths']['canonical'])
+          ? $resource['uri_paths']['canonical']
+          : FALSE;
+
+        // @see https://www.drupal.org/node/2737401
+        // @todo Remove this in Drupal 9.0.0.
+        $old_create_uri_path = !empty($resource['uri_paths']['https://www.drupal.org/link-relations/create'])
+          ? $resource['uri_paths']['https://www.drupal.org/link-relations/create']
+          : FALSE;
+        $new_create_uri_path = !empty($resource['uri_paths']['create'])
+          ? $resource['uri_paths']['create']
+          : FALSE;
+        $create_uri_path = $new_create_uri_path ?: $old_create_uri_path;
+
+        $available_methods = array_intersect(array_map('strtoupper', get_class_methods($resource['class'])), [
+          'HEAD',
+          'GET',
+          'POST',
+          'PUT',
+          'DELETE',
+          'TRACE',
+          'OPTIONS',
+          'CONNECT',
+          'PATCH',
+        ]);
+
+        // @todo Remove this when https://www.drupal.org/node/2300677 is fixed.
+        $is_config_entity = isset($resource['serialization_class']) && is_subclass_of($resource['serialization_class'], \Drupal\Core\Config\Entity\ConfigEntityInterface::class, TRUE);
+        if ($is_config_entity) {
+          $available_methods = array_diff($available_methods, ['POST', 'PATCH', 'DELETE']);
+          $create_uri_path = FALSE;
+        }
+
+        // Now calculate the configured methods: if a RestResourceConfig entity
+        // exists for this @RestResource plugin, then regardless of whether that
+        // configuration is enabled or not, inspect its enabled methods. Strike
+        // through all disabled methods, so that it's clearly conveyed in the UI
+        // which methods are supported on which URL, but may be disabled.
+        if (isset($config[$this->getResourceKey($id)])) {
+          $enabled_methods = $config[$this->getResourceKey($id)]->getMethods();
+          $disabled_methods = array_diff($available_methods, $enabled_methods);
+          $configured_methods = array_merge(
+            array_intersect($available_methods, $enabled_methods),
+            array_map(function ($method) { return "<del>$method</del>"; }, $disabled_methods)
+          );
+        }
+        else {
+          $configured_methods = $available_methods;
+        }
+
+        // All necessary information is collected, now generate some HTML.
+        $canonical_methods = implode(', ', array_diff($configured_methods, ['POST']));
+        if ($canonical_uri_path && $create_uri_path) {
+          $uri_paths = "<code>$canonical_uri_path</code>: $canonical_methods";
+          $uri_paths.= "</br><code>$create_uri_path</code>: POST";
+        }
+        else {
+          if ($canonical_uri_path) {
+            $uri_paths = "<code>$canonical_uri_path</code>: $canonical_methods";
+          }
+          else {
+            $uri_paths = "<code>$create_uri_path</code>: POST";
+          }
+        }
+
+        $list[$status]['table']['#rows'][$id] = [
+          'data' => [
+            'name' => !$is_config_entity ? $resource['label'] : $this->t('@label <sup>(read-only)</sup>', ['@label' => $resource['label']]),
+            'path' => [
+              'data' => [
+                '#type' => 'inline_template',
+                '#template' => $uri_paths,
+              ],
+            ],
+            'description' => [],
+            'operations' => [],
+          ],
+        ];
+
+        if ($status == 'disabled') {
+          $list[$status]['table']['#rows'][$id]['data']['operations']['data'] = [
+            '#type' => 'operations',
+            '#links' => [
+              'enable' => [
+                'title' => $this->t('Enable'),
+                'url' => Url::fromRoute('restui.edit', ['resource_id' => $id]),
+              ],
+            ],
+          ];
+        }
+        else {
+          $list[$status]['table']['#rows'][$id]['data']['operations']['data'] = [
+            '#type' => 'operations',
+            '#links' => [
+              'edit' => [
+                'title' => $this->t('Edit'),
+                'url' => Url::fromRoute('restui.edit', ['resource_id' => $id]),
+
+              ],
+              'disable' => [
+                'title' => $this->t('Disable'),
+                'url' => Url::fromRoute('restui.disable', ['resource_id' => $id]),
+              ],
+              'permissions' => [
+                'title' => $this->t('Permissions'),
+                'url' => Url::fromRoute('user.admin_permissions', [], ['fragment' => 'module-rest']),
+              ],
+            ],
+          ];
+
+          $list[$status]['table']['#rows'][$id]['data']['description']['data'] = [
+            '#theme' => 'restui_resource_info',
+            '#granularity' => $config[$this->getResourceKey($id)]->get('granularity'),
+            '#configuration' => $config[$this->getResourceKey($id)]->get('configuration'),
+          ];
+        }
+      }
+    }
+
+    $list['enabled']['table']['#empty'] = $this->t('There are no enabled resources.');
+    $list['disabled']['table']['#empty'] = $this->t('There are no disabled resources.');
+    $list['#title'] = $this->t('REST resources');
+    return $list;
+  }
+
+  /**
+   * Disables a resource.
+   *
+   * @param string $resource_id
+   *   The identifier or the REST resource.
+   *
+   * @return \Drupal\Core\Ajax\AjaxResponse|\Symfony\Component\HttpFoundation\RedirectResponse
+   *   Redirects back to the listing page.
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
+   *   Access is denied, if the token is invalid or missing.
+   */
+  public function disable($resource_id) {
+    $resources = $this->resourceConfigStorage->loadMultiple();
+
+    if ($resources[$this->getResourceKey($resource_id)]) {
+      $resources[$this->getResourceKey($resource_id)]->disable()->save();
+      drupal_set_message(t('The resource was disabled successfully.'));
+    }
+
+    // Redirect back to the page.
+    return new RedirectResponse($this->urlGenerator->generate('restui.list', [], TRUE));
+  }
+
+  /**
+   * The key used in the form.
+   *
+   * @param string $resource_id
+   *   The resource ID.
+   *
+   * @return string
+   *   The resource key in the form.
+   */
+  protected function getResourceKey($resource_id) {
+    return str_replace(':', '.', $resource_id);
+  }
+
+}
diff --git a/drupal/modules/restui/src/Form/RestUIForm.php b/drupal/modules/restui/src/Form/RestUIForm.php
new file mode 100755
index 0000000..efe5df9
--- /dev/null
+++ b/drupal/modules/restui/src/Form/RestUIForm.php
@@ -0,0 +1,483 @@
+<?php
+
+namespace Drupal\restui\Form;
+
+use Drupal\Core\Authentication\AuthenticationCollectorInterface;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Form\ConfigFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Extension\ModuleHandler;
+use Drupal\rest\Plugin\ResourceInterface;
+use Drupal\rest\RestResourceConfigInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+use Drupal\rest\Plugin\Type\ResourcePluginManager;
+
+/**
+ * Provides a REST resource configuration form.
+ */
+class RestUIForm extends ConfigFormBase {
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandler
+   */
+  protected $moduleHandler;
+
+  /**
+   * The authentication collector.
+   *
+   * @var \Drupal\Core\Authentication\AuthenticationCollectorInterface
+   */
+  protected $authenticationCollector;
+
+  /**
+   * The available serialization formats.
+   *
+   * @var array
+   */
+  protected $formats;
+
+  /**
+   * The REST plugin manager.
+   *
+   * @var \Drupal\rest\Plugin\Type\ResourcePluginManager
+   */
+  protected $resourcePluginManager;
+
+  /**
+   * The REST resource config storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $resourceConfigStorage;
+
+  /**
+   * Constructs a \Drupal\user\RestForm object.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory.
+   * @param \Drupal\Core\Extension\ModuleHandler $module_handler
+   *   The module handler.
+   * @param \Drupal\Core\Authentication\AuthenticationCollectorInterface $authentication_collector
+   *   The authentication collector.
+   * @param array $formats
+   *   The available serialization formats.
+   * @param \Drupal\rest\Plugin\Type\ResourcePluginManager $resourcePluginManager
+   *   The REST plugin manager.
+   * @param \Drupal\Core\Entity\EntityStorageInterface $resource_config_storage
+   *   The REST resource config storage.
+   */
+  public function __construct(ConfigFactoryInterface $config_factory, ModuleHandler $module_handler, AuthenticationCollectorInterface $authentication_collector, array $formats, ResourcePluginManager $resourcePluginManager, EntityStorageInterface $resource_config_storage) {
+    parent::__construct($config_factory);
+    $this->moduleHandler = $module_handler;
+    $this->authenticationCollector = $authentication_collector;
+    $this->formats = $formats;
+    $this->resourcePluginManager = $resourcePluginManager;
+    $this->resourceConfigStorage = $resource_config_storage;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('config.factory'),
+      $container->get('module_handler'),
+      $container->get('authentication_collector'),
+      $container->getParameter('serializer.formats'),
+      $container->get('plugin.manager.rest'),
+      $container->get('entity_type.manager')->getStorage('rest_resource_config')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'restui';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEditableConfigNames() {
+    return [
+      'rest.settings',
+    ];
+  }
+
+  /**
+   * Gets a REST resource config's granularity: from the form, otherwise config.
+   *
+   * @param string $id
+   *   A REST resource config entity ID.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return string
+   *   Either:
+   *   - \Drupal\rest\RestResourceConfigInterface::METHOD_GRANULARITY
+   *   - \Drupal\rest\RestResourceConfigInterface::RESOURCE_GRANULARITY
+   */
+  protected function getGranularity($id, FormStateInterface $form_state) {
+    $granularity = $this->config("rest.resource.{$id}")->get('granularity');
+    if ($form_state->hasValue('granularity')) {
+      $granularity = $form_state->getValue('granularity');
+    }
+    if ($granularity === NULL) {
+      $granularity = RestResourceConfigInterface::RESOURCE_GRANULARITY;
+    }
+    return $granularity;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @param array $form
+   *   The form array.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state.
+   * @param string $resource_id
+   *   A string that identifies the REST resource.
+   *
+   * @return array
+   *   The form structure.
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
+   *   When no plugin found.
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, $resource_id = NULL) {
+    $plugin = $this->resourcePluginManager->createInstance($resource_id);
+    if (empty($plugin)) {
+      throw new NotFoundHttpException();
+    }
+
+    $id = str_replace(':', '.', $resource_id);
+
+    $config = $this->config("rest.resource.{$id}")->get('configuration') ?: [];
+    $pluginDefinition = $plugin->getPluginDefinition();
+    $form['#title'] = $this->t('Settings for resource %label', ['%label' => $pluginDefinition['label']]);
+    $form['#tree'] = TRUE;
+    $form['resource_id'] = ['#type' => 'value', '#value' => $resource_id];
+
+
+    $authentication_providers = array_keys($this->authenticationCollector->getSortedProviders());
+    $authentication_providers = array_combine($authentication_providers, $authentication_providers);
+    $format_options = array_combine($this->formats, $this->formats);
+
+    $granularity = $this->getGranularity($id, $form_state);
+
+    // Granularity selection.
+    $form['granularity'] = [
+      '#title' => t('Granularity'),
+      '#type' => 'select',
+      '#options' => [
+        RestResourceConfigInterface::RESOURCE_GRANULARITY => $this->t('Resource'),
+        RestResourceConfigInterface::METHOD_GRANULARITY => $this->t('Method'),
+      ],
+      '#default_value' => $granularity,
+      '#ajax' => [
+        'callback' => '::processAjaxForm',
+        'wrapper' => 'wrapper',
+      ],
+    ];
+
+    // Wrapper for ajax callback.
+    $form['wrapper'] = [
+      '#type' => 'container',
+      '#attributes' => ['id' => 'wrapper'],
+    ];
+
+    $form['wrapper'] += ($granularity === RestResourceConfigInterface::RESOURCE_GRANULARITY)
+      ? $this->buildConfigurationFormForResourceGranularity($plugin, $authentication_providers, $format_options, $config)
+      : $this->buildConfigurationFormForMethodGranularity($plugin, $authentication_providers, $format_options, $config);
+
+    return parent::buildForm($form, $form_state);
+  }
+
+  /**
+   * Subform constructor when the selected granularity is 'method'.
+   *
+   * @param \Drupal\rest\Plugin\ResourceInterface $plugin
+   *   The REST Resource plugin being configured.
+   * @param array $authentication_providers
+   *   All available authentication providers, to use for #options.
+   * @param array $format_options
+   *   All available formats, to use for #options.
+   * @param array $config
+   *   The current configuration for the REST Resource config entity, or the
+   *   empty array if it does not yet exist.
+   *
+   * @return array
+   *   The subform structure.
+   */
+  protected function buildConfigurationFormForMethodGranularity(ResourceInterface $plugin, array $authentication_providers, array $format_options, array $config) {
+    $methods = $plugin->availableMethods();
+
+    $form = [];
+
+    foreach ($methods as $method) {
+      $group = [];
+      $group[$method] = [
+        '#title' => $method,
+        '#type' => 'checkbox',
+        '#default_value' => isset($config[$method]),
+      ];
+      $group['settings'] = [
+        '#type' => 'container',
+        '#attributes' => ['style' => 'padding-left:20px'],
+      ];
+
+      // Available request formats.
+      $enabled_formats = [];
+      if (isset($config[$method]['supported_formats'])) {
+        $enabled_formats = $config[$method]['supported_formats'];
+      }
+      $method_checkbox_selector = ':input[name="wrapper[methods][' . $method . '][' . $method . ']"]';
+      $states_show_if_method_is_enabled = [
+        'visible' => [$method_checkbox_selector => ['checked' => TRUE]],
+        'invisible' => [$method_checkbox_selector => ['checked' => FALSE]],
+      ];
+      $group['settings']['formats'] = [
+        '#type' => 'checkboxes',
+        '#title' => $this->t('Accepted request formats'),
+        '#options' => $format_options,
+        '#default_value' => $enabled_formats,
+        '#states' => $states_show_if_method_is_enabled,
+      ];
+
+      // Authentication providers.
+      $enabled_auth = [];
+      if (isset($config[$method]['supported_auth'])) {
+        $enabled_auth = $config[$method]['supported_auth'];
+      }
+      $group['settings']['auth'] = [
+        '#title' => $this->t('Authentication providers'),
+        '#type' => 'checkboxes',
+        '#options' => $authentication_providers,
+        '#default_value' => $enabled_auth,
+        '#states' => $states_show_if_method_is_enabled,
+      ];
+      $form['methods'][$method] = $group;
+    }
+    return $form;
+  }
+
+  /**
+   * Subform constructor when the selected granularity is 'resource'.
+   *
+   * @param \Drupal\rest\Plugin\ResourceInterface $plugin
+   *   The REST Resource plugin being configured.
+   * @param array $authentication_providers
+   *   All available authentication providers, to use for #options.
+   * @param array $format_options
+   *   All available formats, to use for #options.
+   * @param array $config
+   *   The current configuration for the REST Resource config entity, or the
+   *   empty array if it does not yet exist.
+   *
+   * @return array
+   *   The subform structure.
+   */
+  protected function buildConfigurationFormForResourceGranularity(ResourceInterface $plugin, array $authentication_providers, array $format_options, array $config) {
+    $methods = $plugin->availableMethods();
+    $method_options = array_combine($methods, $methods);
+
+    $form = [];
+
+    // Methods.
+    $enabled_methods = [];
+    foreach ($methods as $method) {
+      if (isset($config['methods']) && in_array($method, $config['methods'])) {
+        $enabled_methods[$method] = $method;
+      }
+    }
+    $form['settings']['methods'] = [
+      '#type' => 'checkboxes',
+      '#title' => $this->t('Methods'),
+      '#options' => $method_options,
+      '#default_value' => $enabled_methods,
+    ];
+
+    // Formats.
+    $enabled_formats = [];
+    if (isset($config['formats'])) {
+      $enabled_formats = $config['formats'];
+    }
+
+    $form['settings']['formats'] = [
+      '#type' => 'checkboxes',
+      '#title' => $this->t('Accepted request formats'),
+      '#options' => $format_options,
+      '#default_value' => $enabled_formats,
+    ];
+
+    // Authentication providers.
+    $enabled_auth = [];
+    if (isset($config['authentication'])) {
+      $enabled_auth = $config['authentication'];
+    }
+
+    $form['settings']['authentication'] = [
+      '#title' => $this->t('Authentication providers'),
+      '#type' => 'checkboxes',
+      '#options' => $authentication_providers,
+      '#default_value' => $enabled_auth,
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @see \Drupal\rest\Routing\ResourceRoutes::alterRoutes()
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    if ($form_state->getValue('granularity') === RestResourceConfigInterface::RESOURCE_GRANULARITY) {
+      $this->validateFormValuesForResourceGranularity($form_state);
+    }
+    else {
+      $this->validateFormValuesForMethodGranularity($form_state);
+    }
+  }
+
+  /**
+   * Form validation handler when the selected granularity is 'method'.
+   *
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @see \Drupal\restui\Form\RestUIForm::validateForm()
+   */
+  protected function validateFormValuesForMethodGranularity(FormStateInterface $form_state) {
+    // At least one method must be checked.
+    $method_checked = FALSE;
+    foreach ($form_state->getValue(['wrapper', 'methods']) as $method => $values) {
+      if ($values[$method]) {
+        $method_checked = TRUE;
+        // At least one format and authentication provider must be selected.
+        $formats = array_filter($values['settings']['formats']);
+        if (empty($formats)) {
+          $form_state->setErrorByName('methods][' . $method . '][settings][formats', $this->t('At least one format must be selected for method @method.', ['@method' => $method]));
+        }
+        $auth = array_filter($values['settings']['auth']);
+        if (empty($auth)) {
+          $form_state->setErrorByName('methods][' . $method . '][settings][auth', $this->t('At least one authentication provider must be selected for method @method.', ['@method' => $method]));
+        }
+      }
+    }
+    if (!$method_checked) {
+      $form_state->setErrorByName('methods', $this->t('At least one HTTP method must be selected'));
+    }
+  }
+
+  /**
+   * Form validation handler when the selected granularity is 'resource'.
+   *
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @see \Drupal\restui\Form\RestUIForm::validateForm()
+   */
+  protected function validateFormValuesForResourceGranularity(FormStateInterface $form_state) {
+    $settings = $form_state->getValue(['wrapper', 'settings']);
+
+    if (empty(array_filter($settings['methods']))) {
+      $form_state->setErrorByName('methods', $this->t('At least one HTTP method must be selected.'));
+    }
+    if (empty(array_filter($settings['formats']))) {
+      $form_state->setErrorByName('formats', $this->t('At least one request format must be selected.'));
+    }
+    if (empty(array_filter($settings['authentication']))) {
+      $form_state->setErrorByName('authentication', $this->t('At least one authentication provider must be selected'));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $resource_id = str_replace(':', '.', $form_state->getValue('resource_id'));
+    $config = $this->resourceConfigStorage->load($resource_id);
+    $granularity = $form_state->getValue('granularity');
+
+    if (!$config) {
+      $config = $this->resourceConfigStorage->create(['id' => $resource_id]);
+    }
+
+    $configuration = ($granularity === RestResourceConfigInterface::RESOURCE_GRANULARITY)
+      ? static::getConfigurationForResourceGranularity($form_state)
+      : static::getConfigurationForMethodGranularity($form_state);
+
+    $config->set('granularity', $granularity);
+    $config->set('configuration', $configuration);
+    $config->enable();
+    $config->save();
+
+    drupal_set_message($this->t('The resource has been updated.'));
+    // Redirect back to the listing.
+    $form_state->setRedirect('restui.list');
+  }
+
+  /**
+   * Calculates the REST resource configuration when granularity is 'method'.
+   *
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return array
+   *   The value for the 'configuration' key in a REST Resource config entity.
+   */
+  protected static function getConfigurationForMethodGranularity(FormStateInterface $form_state) {
+    $configuration = [];
+    $methods = $form_state->getValue(['wrapper', 'methods']);
+    foreach ($methods as $method => $settings) {
+      if ($settings[$method]) {
+        $configuration[$method] = [
+          'supported_formats' => array_keys(array_filter($settings['settings']['formats'])),
+          'supported_auth' => array_keys(array_filter($settings['settings']['auth'])),
+        ];
+      }
+      else {
+        unset($configuration[$method]);
+      }
+    }
+    return $configuration;
+  }
+
+  /**
+   * Calculates the REST resource configuration when granularity is 'resource'.
+   *
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return array
+   *   The value for the 'configuration' key in a REST Resource config entity.
+   */
+  protected static function getConfigurationForResourceGranularity(FormStateInterface $form_state) {
+    $settings = $form_state->getValue(['wrapper', 'settings']);
+    $configuration = [
+      'methods' => array_keys(array_filter($settings['methods'])),
+      'formats' => array_keys(array_filter($settings['formats'])),
+      'authentication' => array_keys(array_filter($settings['authentication'])),
+    ];
+    return $configuration;
+  }
+
+  /**
+   * Return the settings part of the form when rebuilding through ajax.
+   *
+   * @param array $form
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *
+   * @return array
+   */
+  public function processAjaxForm(array $form, FormStateInterface &$form_state) {
+    return $form['wrapper'];
+  }
+
+}
diff --git a/drupal/modules/restui/templates/restui-resource-info.html.twig b/drupal/modules/restui/templates/restui-resource-info.html.twig
new file mode 100755
index 0000000..92257a7
--- /dev/null
+++ b/drupal/modules/restui/templates/restui-resource-info.html.twig
@@ -0,0 +1,27 @@
+{#
+/**
+ * @file
+ * Default theme implementation for basic administrative info about a REST resource.
+ *
+ * Available variables:
+ * - granularity: string REST resource granularity. Either
+ *   \Drupal\rest\RestResourceConfigInterface::METHOD_GRANULARITY or
+ *   \Drupal\rest\RestResourceConfigInterface::RESOURCE_GRANULARITY.
+ * - configuration: array REST resource config.
+ *
+ * @ingroup themeable
+ */
+#}
+{% if granularity == 'resource' %}
+    <p>methods: {{ configuration.methods|join(', ') }}</br>
+        formats: {{ configuration.formats|join(', ') }}</br>
+        authentication: {{ configuration.authentication|join(', ') }}
+    </p>
+{% else %}
+    {% for method, properties in configuration %}
+        <p>{{ method }}</br>
+            formats: {{ properties.supported_formats|join(', ') }}</br>
+            authentication: {{ properties.supported_auth|join(', ') }}
+        </p>
+    {% endfor %}
+{% endif %}
diff --git a/drupal/modules/restui/tests/src/Functional/RestUITest.php b/drupal/modules/restui/tests/src/Functional/RestUITest.php
new file mode 100755
index 0000000..2f5d9d2
--- /dev/null
+++ b/drupal/modules/restui/tests/src/Functional/RestUITest.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace Drupal\Tests\restui\Functional;
+
+use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
+use Drupal\rest\RestResourceConfigInterface;
+
+/**
+ * Tests Rest UI functionality.
+ *
+ * @group restui
+ */
+class RestUITest extends JavascriptTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['node', 'restui'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    // Create a user with permissions to manage.
+    $permissions = [
+      'administer site configuration',
+      'administer rest resources'
+    ];
+    $account = $this->drupalCreateUser($permissions);
+
+    // Initiate user session.
+    $this->drupalLogin($account);
+  }
+
+  /**
+   * Tests enabling a resource and accessing it.
+   */
+  public function testConsumers() {
+    // Check that user can access the administration interface.
+    $this->drupalGet('admin/config/services/rest');
+    $this->assertEquals(200, $this->getSession()->getStatusCode());
+
+    // Get configuration page for Node resource.
+    $this->drupalGet('admin/config/services/rest/resource/entity%3Anode/edit');
+    $page = $this->getSession()->getPage();
+
+    // Assert that the 'resource' configuration form is build as default.
+    $this->assertSession()->fieldExists('wrapper[settings][methods][GET]');
+    $this->assertSession()->fieldExists('wrapper[settings][formats][json]');
+    $this->assertSession()->fieldExists('wrapper[settings][authentication][cookie]');
+
+    // Method granularity.
+    // Adjust the node resource so it allows GET method with JSON format and
+    // Cookie authentication.
+    $page->findField('granularity')->selectOption(RestResourceConfigInterface::METHOD_GRANULARITY);
+    $this->assertSession()->waitForField('wrapper[methods][GET]');
+    $page->findField('wrapper[methods][GET][GET]')->check();
+    $page->findField('wrapper[methods][GET][settings][formats][json]')->check();
+    $page->findField('wrapper[methods][GET][settings][auth][cookie]')->check();
+
+    $page->pressButton('Save configuration');
+    $this->assertSession()->pageTextContains('The resource has been updated.');
+
+    // Resource granularity.
+    // Adjust the node resource so it allows GET method, JSON format and
+    // Cookie authentication.
+    $this->drupalGet('admin/config/services/rest/resource/entity%3Anode/edit');
+
+    $page = $this->getSession()->getPage();
+    $page->findField('granularity')->selectOption(RestResourceConfigInterface::RESOURCE_GRANULARITY);
+    $this->assertSession()->waitForField('wrapper[settings][methods][GET]');
+    $page->findField('wrapper[settings][methods][GET]')->check();
+    $page->findField('wrapper[settings][formats][json]')->check();
+    $page->findField('wrapper[settings][authentication][cookie]')->check();
+
+    $page->pressButton('Save configuration');
+    $this->assertSession()->pageTextContains('The resource has been updated.');
+  }
+
+}
-- 
GitLab