keycloak-memoizeit
Changes
examples/providers/domain-extension/pom.xml 64(+64 -0)
examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/CompanyRepresentation.java 33(+33 -0)
examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/Company.java 66(+66 -0)
examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/ExampleJpaEntityProvider.java 50(+50 -0)
examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/ExampleJpaEntityProviderFactory.java 57(+57 -0)
examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/CompanyResource.java 52(+52 -0)
examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRealmResourceProvider.java 40(+40 -0)
examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRealmResourceProviderFactory.java 52(+52 -0)
examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRestResource.java 43(+43 -0)
examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleService.java 33(+33 -0)
examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleServiceProviderFactory.java 24(+24 -0)
examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleSpi.java 47(+47 -0)
examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/impl/ExampleServiceImpl.java 88(+88 -0)
examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/impl/ExampleServiceProviderFactoryImpl.java 53(+53 -0)
examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory 18(+18 -0)
examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.examples.domainextension.spi.ExampleServiceProviderFactory 18(+18 -0)
examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.provider.Spi 18(+18 -0)
examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory 18(+18 -0)
examples/providers/pom.xml 1(+1 -0)
model/jpa/pom.xml 5(+5 -0)
model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java 10(+1 -9)
model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntityProvider.java 53(+53 -0)
model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntityProviderFactory.java 29(+29 -0)
model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/ProxyClassLoader.java 87(+87 -0)
model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/DefaultLiquibaseConnectionProvider.java 46(+31 -15)
model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/LiquibaseConnectionProvider.java 2(+2 -0)
model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java 104(+75 -29)
testsuite/integration/pom.xml 4(+4 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/CompanyRepresentation.java 51(+51 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/jpa/Company.java 66(+66 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/jpa/ExampleJpaEntityProvider.java 50(+50 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/jpa/ExampleJpaEntityProviderFactory.java 57(+57 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/rest/CompanyResource.java 77(+77 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/rest/ExampleRealmResourceProvider.java 40(+40 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/rest/ExampleRealmResourceProviderFactory.java 52(+52 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/rest/ExampleRestResource.java 59(+59 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/ExampleService.java 35(+35 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/ExampleServiceProviderFactory.java 24(+24 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/ExampleSpi.java 47(+47 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/impl/ExampleServiceImpl.java 100(+100 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/impl/ExampleServiceProviderFactoryImpl.java 53(+53 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/example-changelog.xml 25(+25 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory 18(+18 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.provider.Spi 18(+18 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory 3(+2 -1)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.testsuite.domainextension.spi.ExampleServiceProviderFactory 18(+18 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/org/keycloak/testsuite/integration-arquillian-testsuite-providers/main/module.xml 6(+5 -1)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/KeycloakTestingClient.java 3(+3 -0)
Details
diff --git a/examples/providers/domain-extension/invoke-authenticated.sh b/examples/providers/domain-extension/invoke-authenticated.sh
new file mode 100755
index 0000000..19b56b1
--- /dev/null
+++ b/examples/providers/domain-extension/invoke-authenticated.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+export DIRECT_GRANT_RESPONSE=$(curl -i --request POST http://localhost:8080/auth/realms/master/protocol/openid-connect/token --header "Accept: application/json" --header "Content-Type: application/x-www-form-urlencoded" --data "grant_type=password&username=admin&password=admin&client_id=admin-cli")
+
+echo -e "\n\nSENT RESOURCE-OWNER-PASSWORD-CREDENTIALS-REQUEST. OUTPUT IS:\n\n";
+echo $DIRECT_GRANT_RESPONSE;
+
+export ACCESS_TOKEN=$(echo $DIRECT_GRANT_RESPONSE | grep "access_token" | sed 's/.*\"access_token\":\"\([^\"]*\)\".*/\1/g');
+echo -e "\n\nACCESS TOKEN IS \"$ACCESS_TOKEN\"";
+
+echo -e "\n\nSENDING UN-AUTHENTICATED REQUEST. THIS SHOULD FAIL WITH 401: ";
+curl -i --request POST http://localhost:8080/auth/realms/master/example/companies-auth --data "{ \"name\": \"auth foo company\" }" --header "Content-type: application/json"
+
+echo -e "\n\nSENDING AUTHENTICATED REQUEST. THIS SHOULD SUCCESSFULY CREATE COMPANY AND SUCCESS WITH 201: ";
+curl -i --request POST http://localhost:8080/auth/realms/master/example/companies-auth --data "{ \"name\": \"auth foo company\" }" --header "Content-type: application/json" --header "Authorization: Bearer $ACCESS_TOKEN";
+
+echo -e "\n\nSEARCH COMPANIES: ";
+curl -i --request GET http://localhost:8080/auth/realms/master/example/companies-auth --header "Accept: application/json" --header "Authorization: Bearer $ACCESS_TOKEN";
+
examples/providers/domain-extension/pom.xml 64(+64 -0)
diff --git a/examples/providers/domain-extension/pom.xml b/examples/providers/domain-extension/pom.xml
new file mode 100755
index 0000000..8d8e496
--- /dev/null
+++ b/examples/providers/domain-extension/pom.xml
@@ -0,0 +1,64 @@
+<!--
+ ~ Copyright 2016 Red Hat, Inc. and/or its affiliates
+ ~ and other contributors as indicated by the @author tags.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <parent>
+ <artifactId>keycloak-examples-providers-parent</artifactId>
+ <groupId>org.keycloak</groupId>
+ <version>2.0.0.CR1-SNAPSHOT</version>
+ </parent>
+
+ <name>Domain Extension Example</name>
+ <description/>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>keycloak-examples-providers-domain-extension</artifactId>
+ <packaging>jar</packaging>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-services</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-server-spi</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-model-jpa</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.spec.javax.ws.rs</groupId>
+ <artifactId>jboss-jaxrs-api_2.0_spec</artifactId>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <finalName>domain-extension-example</finalName>
+ </build>
+</project>
+
diff --git a/examples/providers/domain-extension/README.md b/examples/providers/domain-extension/README.md
new file mode 100644
index 0000000..e1aa2cd
--- /dev/null
+++ b/examples/providers/domain-extension/README.md
@@ -0,0 +1,44 @@
+Example Domain Extension
+========================
+
+To run, deploy as a module by running:
+
+ $KEYCLOAK_HOME/bin/jboss-cli.sh --command="module add --name=org.keycloak.examples.domain-extension-example --resources=target/domain-extension-example.jar --dependencies=org.keycloak.keycloak-core,org.keycloak.keycloak-services,org.keycloak.keycloak-model-jpa,org.keycloak.keycloak-server-spi,javax.ws.rs.api,javax.persistence.api,org.hibernate,org.javassist"
+
+
+Then registering the provider by editing keycloak-server.json and adding the module to the providers field:
+
+ "providers": [
+ ....
+ "module:org.keycloak.examples.domain-extension-example"
+ ],
+
+Then start (or restart) the server.
+
+Testing
+-------
+First you can create some example companies with these CURL requests.
+
+````
+curl -i --request POST http://localhost:8080/auth/realms/master/example/companies --data "{ \"name\": \"foo company\" }" --header "Content-type: application/json"
+curl -i --request POST http://localhost:8080/auth/realms/master/example/companies --data "{ \"name\": \"bar company\" }" --header "Content-type: application/json"
+````
+
+Then you can lookup all companies
+
+````
+curl -i --request GET http://localhost:8080/auth/realms/master/example/companies --header "Accept: application/json"
+````
+
+If you create realm `foo` in Keycloak admin console and then replace the realm name in the URI (for example like `http://localhost:8080/auth/realms/foo/example/companies` ) you will see
+that companies are scoped per-realm. So you will see different companies for realm `master` and for realm `foo` .
+
+
+Testing with authenticated access
+---------------------------------
+Example contains the endpoint, which is accessible just for authenticated users. REST request must be authenticated with bearer access token
+of authenticated user and the user must be in realm role `admin` in order to access the resource. You can run bash script from the current directory:
+````
+./invoke-authenticated.sh
+````
+The script assumes user `admin` with password `admin` exists in realm `master`. Also it assumes that you have `curl` installed.
\ No newline at end of file
diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/CompanyRepresentation.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/CompanyRepresentation.java
new file mode 100644
index 0000000..2b76d57
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/CompanyRepresentation.java
@@ -0,0 +1,33 @@
+package org.keycloak.examples.domainextension;
+
+import org.keycloak.examples.domainextension.jpa.Company;
+
+public class CompanyRepresentation {
+
+ private String id;
+ private String name;
+
+ public CompanyRepresentation() {
+ }
+
+ public CompanyRepresentation(Company company) {
+ id = company.getId();
+ name = company.getName();
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+}
diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/Company.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/Company.java
new file mode 100644
index 0000000..bca8e1d
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/Company.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.examples.domainextension.jpa;
+
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.Table;
+
+@Entity
+@Table(name = "EXAMPLE_COMPANY")
+@NamedQueries({ @NamedQuery(name = "findByRealm", query = "from Company where realmId = :realmId") })
+public class Company {
+
+ @Id
+ @Column(name = "ID")
+ private String id;
+
+ @Column(name = "NAME", nullable = false)
+ private String name;
+
+ @Column(name = "REALM_ID", nullable = false)
+ private String realmId;
+
+ public String getId() {
+ return id;
+ }
+
+ public String getRealmId() {
+ return realmId;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public void setRealmId(String realmId) {
+ this.realmId = realmId;
+ }
+}
diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/ExampleJpaEntityProvider.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/ExampleJpaEntityProvider.java
new file mode 100644
index 0000000..b6529fd
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/ExampleJpaEntityProvider.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.examples.domainextension.jpa;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider;
+
+/**
+ * @author <a href="mailto:erik.mulder@docdatapayments.com">Erik Mulder</a>
+ *
+ * Example JpaEntityProvider.
+ */
+public class ExampleJpaEntityProvider implements JpaEntityProvider {
+
+ @Override
+ public List<Class<?>> getEntities() {
+ return Collections.<Class<?>>singletonList(Company.class);
+ }
+
+ @Override
+ public String getChangelogLocation() {
+ return "META-INF/example-changelog.xml";
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public String getFactoryId() {
+ return ExampleJpaEntityProviderFactory.ID;
+ }
+}
diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/ExampleJpaEntityProviderFactory.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/ExampleJpaEntityProviderFactory.java
new file mode 100644
index 0000000..2c919f4
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/ExampleJpaEntityProviderFactory.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.examples.domainextension.jpa;
+
+import org.keycloak.Config.Scope;
+import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider;
+import org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
+/**
+ * @author <a href="mailto:erik.mulder@docdatapayments.com">Erik Mulder</a>
+ *
+ * Example JpaEntityProviderFactory.
+ */
+public class ExampleJpaEntityProviderFactory implements JpaEntityProviderFactory {
+
+ protected static final String ID = "example-entity-provider";
+
+ @Override
+ public JpaEntityProvider create(KeycloakSession session) {
+ return new ExampleJpaEntityProvider();
+ }
+
+ @Override
+ public String getId() {
+ return ID;
+ }
+
+ @Override
+ public void init(Scope config) {
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ }
+
+ @Override
+ public void close() {
+ }
+
+}
diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/CompanyResource.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/CompanyResource.java
new file mode 100644
index 0000000..ba98978
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/CompanyResource.java
@@ -0,0 +1,52 @@
+package org.keycloak.examples.domainextension.rest;
+
+import java.util.List;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.keycloak.examples.domainextension.CompanyRepresentation;
+import org.keycloak.examples.domainextension.spi.ExampleService;
+import org.keycloak.models.KeycloakSession;
+
+public class CompanyResource {
+
+ private final KeycloakSession session;
+
+ public CompanyResource(KeycloakSession session) {
+ this.session = session;
+ }
+
+ @GET
+ @Path("")
+ @NoCache
+ @Produces(MediaType.APPLICATION_JSON)
+ public List<CompanyRepresentation> getCompanies() {
+ return session.getProvider(ExampleService.class).listCompanies();
+ }
+
+ @POST
+ @Path("")
+ @NoCache
+ @Consumes(MediaType.APPLICATION_JSON)
+ public Response createCompany(CompanyRepresentation rep) {
+ session.getProvider(ExampleService.class).addCompany(rep);
+ return Response.created(session.getContext().getUri().getAbsolutePathBuilder().path(rep.getId()).build()).build();
+ }
+
+ @GET
+ @NoCache
+ @Path("{id}")
+ @Produces(MediaType.APPLICATION_JSON)
+ public CompanyRepresentation getCompany(@PathParam("id") final String id) {
+ return session.getProvider(ExampleService.class).findCompany(id);
+ }
+
+}
\ No newline at end of file
diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRealmResourceProvider.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRealmResourceProvider.java
new file mode 100644
index 0000000..0882d22
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRealmResourceProvider.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.examples.domainextension.rest;
+
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.services.resource.RealmResourceProvider;
+
+public class ExampleRealmResourceProvider implements RealmResourceProvider {
+
+ private KeycloakSession session;
+
+ public ExampleRealmResourceProvider(KeycloakSession session) {
+ this.session = session;
+ }
+
+ @Override
+ public Object getResource() {
+ return new ExampleRestResource(session);
+ }
+
+ @Override
+ public void close() {
+ }
+
+}
diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRealmResourceProviderFactory.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRealmResourceProviderFactory.java
new file mode 100644
index 0000000..33c2ca2
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRealmResourceProviderFactory.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.examples.domainextension.rest;
+
+import org.keycloak.Config.Scope;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.services.resource.RealmResourceProvider;
+import org.keycloak.services.resource.RealmResourceProviderFactory;
+
+public class ExampleRealmResourceProviderFactory implements RealmResourceProviderFactory {
+
+ public static final String ID = "example";
+
+ @Override
+ public String getId() {
+ return ID;
+ }
+
+ @Override
+ public RealmResourceProvider create(KeycloakSession session) {
+ return new ExampleRealmResourceProvider(session);
+ }
+
+ @Override
+ public void init(Scope config) {
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ }
+
+ @Override
+ public void close() {
+ }
+
+}
diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRestResource.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRestResource.java
new file mode 100644
index 0000000..db774cf
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRestResource.java
@@ -0,0 +1,43 @@
+package org.keycloak.examples.domainextension.rest;
+
+import javax.ws.rs.ForbiddenException;
+import javax.ws.rs.NotAuthorizedException;
+import javax.ws.rs.Path;
+
+
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.services.managers.AppAuthManager;
+import org.keycloak.services.managers.AuthenticationManager;
+
+public class ExampleRestResource {
+
+ private final KeycloakSession session;
+ private final AuthenticationManager.AuthResult auth;
+
+ public ExampleRestResource(KeycloakSession session) {
+ this.session = session;
+ this.auth = new AppAuthManager().authenticateBearerToken(session, session.getContext().getRealm());
+ }
+
+ @Path("companies")
+ public CompanyResource getCompanyResource() {
+ return new CompanyResource(session);
+ }
+
+ // Same like "companies" endpoint, but REST endpoint is authenticated with Bearer token and user must be in realm role "admin"
+ // Just for illustration purposes
+ @Path("companies-auth")
+ public CompanyResource getCompanyResourceAuthenticated() {
+ checkRealmAdmin();
+ return new CompanyResource(session);
+ }
+
+ private void checkRealmAdmin() {
+ if (auth == null) {
+ throw new NotAuthorizedException("Bearer");
+ } else if (auth.getToken().getRealmAccess() == null || !auth.getToken().getRealmAccess().isUserInRole("admin")) {
+ throw new ForbiddenException("Does not have realm admin role");
+ }
+ }
+
+}
diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleService.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleService.java
new file mode 100644
index 0000000..7f41327
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleService.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.examples.domainextension.spi;
+
+import java.util.List;
+
+import org.keycloak.examples.domainextension.CompanyRepresentation;
+import org.keycloak.provider.Provider;
+
+public interface ExampleService extends Provider {
+
+ List<CompanyRepresentation> listCompanies();
+
+ CompanyRepresentation findCompany(String id);
+
+ CompanyRepresentation addCompany(CompanyRepresentation company);
+
+}
diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleServiceProviderFactory.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleServiceProviderFactory.java
new file mode 100644
index 0000000..2c6a122
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleServiceProviderFactory.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.examples.domainextension.spi;
+
+import org.keycloak.provider.ProviderFactory;
+
+public interface ExampleServiceProviderFactory extends ProviderFactory<ExampleService> {
+
+}
diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleSpi.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleSpi.java
new file mode 100644
index 0000000..811ec92
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleSpi.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.examples.domainextension.spi;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+public class ExampleSpi implements Spi {
+
+ @Override
+ public boolean isInternal() {
+ return false;
+ }
+
+ @Override
+ public String getName() {
+ return "example";
+ }
+
+ @Override
+ public Class<? extends Provider> getProviderClass() {
+ return ExampleService.class;
+ }
+
+ @Override
+ @SuppressWarnings("rawtypes")
+ public Class<? extends ProviderFactory> getProviderFactoryClass() {
+ return ExampleServiceProviderFactory.class;
+ }
+
+}
diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/impl/ExampleServiceImpl.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/impl/ExampleServiceImpl.java
new file mode 100644
index 0000000..49cc228
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/impl/ExampleServiceImpl.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.examples.domainextension.spi.impl;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.persistence.EntityManager;
+
+import org.keycloak.connections.jpa.JpaConnectionProvider;
+import org.keycloak.examples.domainextension.jpa.Company;
+import org.keycloak.examples.domainextension.CompanyRepresentation;
+import org.keycloak.examples.domainextension.spi.ExampleService;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+
+public class ExampleServiceImpl implements ExampleService {
+
+ private final KeycloakSession session;
+
+ public ExampleServiceImpl(KeycloakSession session) {
+ this.session = session;
+ if (getRealm() == null) {
+ throw new IllegalStateException("The service cannot accept a session without a realm in it's context.");
+ }
+ }
+
+ private EntityManager getEntityManager() {
+ return session.getProvider(JpaConnectionProvider.class).getEntityManager();
+ }
+
+ protected RealmModel getRealm() {
+ return session.getContext().getRealm();
+ }
+
+ @Override
+ public List<CompanyRepresentation> listCompanies() {
+ List<Company> companyEntities = getEntityManager().createNamedQuery("findByRealm", Company.class)
+ .setParameter("realmId", getRealm().getId())
+ .getResultList();
+
+ List<CompanyRepresentation> result = new LinkedList<>();
+ for (Company entity : companyEntities) {
+ result.add(new CompanyRepresentation(entity));
+ }
+ return result;
+ }
+
+ @Override
+ public CompanyRepresentation findCompany(String id) {
+ Company entity = getEntityManager().find(Company.class, id);
+ return entity==null ? null : new CompanyRepresentation(entity);
+ }
+
+ @Override
+ public CompanyRepresentation addCompany(CompanyRepresentation company) {
+ Company entity = new Company();
+ String id = company.getId()==null ? KeycloakModelUtils.generateId() : company.getId();
+ entity.setId(id);
+ entity.setName(company.getName());
+ entity.setRealmId(getRealm().getId());
+ getEntityManager().persist(entity);
+
+ company.setId(id);
+ return company;
+ }
+
+ public void close() {
+ // Nothing to do.
+ }
+
+}
diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/impl/ExampleServiceProviderFactoryImpl.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/impl/ExampleServiceProviderFactoryImpl.java
new file mode 100644
index 0000000..e4e2ddf
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/impl/ExampleServiceProviderFactoryImpl.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.examples.domainextension.spi.impl;
+
+import org.keycloak.Config.Scope;
+import org.keycloak.examples.domainextension.spi.ExampleService;
+import org.keycloak.examples.domainextension.spi.ExampleServiceProviderFactory;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
+public class ExampleServiceProviderFactoryImpl implements ExampleServiceProviderFactory {
+
+ @Override
+ public ExampleService create(KeycloakSession session) {
+ return new ExampleServiceImpl(session);
+ }
+
+ @Override
+ public void init(Scope config) {
+
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @Override
+ public String getId() {
+ return "exampleServiceImpl";
+ }
+
+}
diff --git a/examples/providers/domain-extension/src/main/resources/META-INF/example-changelog.xml b/examples/providers/domain-extension/src/main/resources/META-INF/example-changelog.xml
new file mode 100644
index 0000000..5edd719
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/resources/META-INF/example-changelog.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
+ <changeSet author="erik.mulder@docdatapayments.com" id="example-1.0">
+
+ <createTable tableName="EXAMPLE_COMPANY">
+ <column name="ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="NAME" type="VARCHAR(255)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="REALM_ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ </createTable>
+
+ <addPrimaryKey
+ constraintName="PK_COMPANY"
+ tableName="EXAMPLE_COMPANY"
+ columnNames="ID"
+ />
+
+ </changeSet>
+
+</databaseChangeLog>
diff --git a/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory b/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory
new file mode 100644
index 0000000..c7b88db
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory
@@ -0,0 +1,18 @@
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.keycloak.examples.domainextension.jpa.ExampleJpaEntityProviderFactory
diff --git a/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.examples.domainextension.spi.ExampleServiceProviderFactory b/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.examples.domainextension.spi.ExampleServiceProviderFactory
new file mode 100644
index 0000000..57f9f89
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.examples.domainextension.spi.ExampleServiceProviderFactory
@@ -0,0 +1,18 @@
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.keycloak.examples.domainextension.spi.impl.ExampleServiceProviderFactoryImpl
diff --git a/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.provider.Spi
new file mode 100644
index 0000000..e013bbd
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -0,0 +1,18 @@
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.keycloak.examples.domainextension.spi.ExampleSpi
diff --git a/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory b/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory
new file mode 100644
index 0000000..ea81617
--- /dev/null
+++ b/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory
@@ -0,0 +1,18 @@
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.keycloak.examples.domainextension.rest.ExampleRealmResourceProviderFactory
\ No newline at end of file
examples/providers/pom.xml 1(+1 -0)
diff --git a/examples/providers/pom.xml b/examples/providers/pom.xml
index 9557807..7153c45 100755
--- a/examples/providers/pom.xml
+++ b/examples/providers/pom.xml
@@ -36,5 +36,6 @@
<module>federation-provider</module>
<module>authenticator</module>
<module>rest</module>
+ <module>domain-extension</module>
</modules>
</project>
model/jpa/pom.xml 5(+5 -0)
diff --git a/model/jpa/pom.xml b/model/jpa/pom.xml
index 47035c5..e1c8742 100755
--- a/model/jpa/pom.xml
+++ b/model/jpa/pom.xml
@@ -102,6 +102,11 @@
<artifactId>jackson-core</artifactId>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
<plugins>
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java
index 9c93214..b5621ef 100755
--- a/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java
@@ -20,25 +20,17 @@ package org.keycloak.connections.jpa;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
-import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.LinkedHashMap;
-import java.util.List;
import java.util.Map;
import javax.naming.InitialContext;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
-import javax.persistence.Persistence;
-import javax.persistence.spi.PersistenceUnitTransactionType;
import javax.sql.DataSource;
-import org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl;
import org.hibernate.ejb.AvailableSettings;
-import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor;
-import org.hibernate.jpa.boot.internal.PersistenceXmlParser;
-import org.hibernate.jpa.boot.spi.Bootstrap;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.connections.jpa.updater.JpaUpdaterProvider;
@@ -182,7 +174,7 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
}
logger.trace("Creating EntityManagerFactory");
- emf = JpaUtils.createEntityManagerFactory(unitName, properties, getClass().getClassLoader());
+ emf = JpaUtils.createEntityManagerFactory(session, unitName, properties, getClass().getClassLoader());
logger.trace("EntityManagerFactory created");
if (globalStatsInterval != -1) {
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntityProvider.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntityProvider.java
new file mode 100644
index 0000000..567080e
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntityProvider.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.connections.jpa.entityprovider;
+
+import java.util.List;
+
+import org.keycloak.provider.Provider;
+
+/**
+ * @author <a href="mailto:erik.mulder@docdatapayments.com">Erik Mulder</a>
+ *
+ * A JPA Entity Provider can supply extra JPA entities that the Keycloak system should include in it's entity manager. The
+ * entities should be provided as a list of Class objects.
+ */
+public interface JpaEntityProvider extends Provider {
+
+ /**
+ * Return the entities that should be added to the entity manager.
+ *
+ * @return list of class objects
+ */
+ List<Class<?>> getEntities();
+
+ /**
+ * Return the location of the Liquibase changelog that facilitates the extra JPA entities.
+ * This should be a location that can be found on the same classpath as the entity classes.
+ *
+ * @return a changelog location or null if not needed
+ */
+ String getChangelogLocation();
+
+ /**
+ * Return the ID of provider factory, which created this provider. Might be used to "compute" the table name of liquibase changelog table.
+ * @return ID of provider factory
+ */
+ String getFactoryId();
+
+}
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntityProviderFactory.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntityProviderFactory.java
new file mode 100644
index 0000000..8c08bf9
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntityProviderFactory.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.connections.jpa.entityprovider;
+
+import org.keycloak.provider.ProviderFactory;
+
+/**
+ * @author <a href="mailto:erik.mulder@docdatapayments.com">Erik Mulder</a>
+ *
+ * Extended interface for a provider factory for JpaEntityProvider's.
+ */
+public interface JpaEntityProviderFactory extends ProviderFactory<JpaEntityProvider> {
+
+}
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntitySpi.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntitySpi.java
new file mode 100644
index 0000000..d89389f
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntitySpi.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.connections.jpa.entityprovider;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+/**
+ * @author <a href="mailto:erik.mulder@docdatapayments.com">Erik Mulder</a>
+ *
+ * Spi that allows for adding extra JPA entity's to the Keycloak entity manager.
+ */
+public class JpaEntitySpi implements Spi {
+
+ @Override
+ public boolean isInternal() {
+ return false;
+ }
+
+ @Override
+ public String getName() {
+ return "jpa-entity-provider";
+ }
+
+ @Override
+ public Class<? extends Provider> getProviderClass() {
+ return JpaEntityProvider.class;
+ }
+
+ @Override
+ public Class<? extends ProviderFactory> getProviderFactoryClass() {
+ return JpaEntityProviderFactory.class;
+ }
+
+}
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/ProxyClassLoader.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/ProxyClassLoader.java
new file mode 100644
index 0000000..e9b82a3
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/ProxyClassLoader.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.connections.jpa.entityprovider;
+
+import java.net.URL;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:erik.mulder@docdatapayments.com">Erik Mulder</a>
+ *
+ * Classloader implementation to facilitate loading classes and resources from a collection of other classloaders.
+ * Effectively it forms a proxy to one or more other classloaders.
+ *
+ * The way it works:
+ * - Get all (unique) classloaders from all provided classes
+ * - For each class or resource that is 'requested':
+ * - First try all provided classloaders and if we have a match, return that
+ * - If no match was found: proceed with 'normal' classloading in 'current classpath' scope
+ *
+ * In this particular context: only loadClass and getResource overrides are needed, since those
+ * are the methods that a classloading and resource loading process will need.
+ */
+public class ProxyClassLoader extends ClassLoader {
+
+ private Set<ClassLoader> classloaders;
+
+ public ProxyClassLoader(Collection<Class<?>> classes, ClassLoader parentClassLoader) {
+ super(parentClassLoader);
+ init(classes);
+ }
+
+ public ProxyClassLoader(Collection<Class<?>> classes) {
+ init(classes);
+ }
+
+ private void init(Collection<Class<?>> classes) {
+ classloaders = new HashSet<>();
+ for (Class<?> clazz : classes) {
+ classloaders.add(clazz.getClassLoader());
+ }
+ }
+
+ @Override
+ public Class<?> loadClass(String name) throws ClassNotFoundException {
+ for (ClassLoader classloader : classloaders) {
+ try {
+ return classloader.loadClass(name);
+ } catch (ClassNotFoundException e) {
+ // This particular class loader did not find the class. It's expected behavior that
+ // this can happen, so we'll just ignore the exception and let the next one try.
+ }
+ }
+ // We did not find the class in the proxy class loaders, so proceed with 'normal' behavior.
+ return super.loadClass(name);
+ }
+
+ @Override
+ public URL getResource(String name) {
+ for (ClassLoader classloader : classloaders) {
+ URL resource = classloader.getResource(name);
+ if (resource != null) {
+ return resource;
+ }
+ // Resource == null means not found, so let the next one try.
+ }
+ // We could not get the resource from the proxy class loaders, so proceed with 'normal' behavior.
+ return super.getResource(name);
+ }
+
+}
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/DefaultLiquibaseConnectionProvider.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/DefaultLiquibaseConnectionProvider.java
index b9018f8..5a58702 100644
--- a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/DefaultLiquibaseConnectionProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/DefaultLiquibaseConnectionProvider.java
@@ -19,6 +19,16 @@ package org.keycloak.connections.jpa.updater.liquibase.conn;
import java.sql.Connection;
+import org.jboss.logging.Logger;
+import org.keycloak.Config;
+import org.keycloak.connections.jpa.updater.liquibase.LiquibaseJpaUpdaterProvider;
+import org.keycloak.connections.jpa.updater.liquibase.PostgresPlusDatabase;
+import org.keycloak.connections.jpa.updater.liquibase.lock.CustomInsertLockRecordGenerator;
+import org.keycloak.connections.jpa.updater.liquibase.lock.CustomLockDatabaseChangeLogGenerator;
+import org.keycloak.connections.jpa.updater.liquibase.lock.DummyLockService;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
import liquibase.Liquibase;
import liquibase.changelog.ChangeSet;
import liquibase.changelog.DatabaseChangeLog;
@@ -27,23 +37,12 @@ import liquibase.database.DatabaseFactory;
import liquibase.database.core.DB2Database;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.LiquibaseException;
-import liquibase.lockservice.LockService;
-import liquibase.lockservice.LockServiceFactory;
import liquibase.logging.LogFactory;
import liquibase.logging.LogLevel;
import liquibase.resource.ClassLoaderResourceAccessor;
+import liquibase.resource.ResourceAccessor;
import liquibase.servicelocator.ServiceLocator;
import liquibase.sqlgenerator.SqlGeneratorFactory;
-import org.jboss.logging.Logger;
-import org.keycloak.Config;
-import org.keycloak.connections.jpa.updater.liquibase.LiquibaseJpaUpdaterProvider;
-import org.keycloak.connections.jpa.updater.liquibase.PostgresPlusDatabase;
-import org.keycloak.connections.jpa.updater.liquibase.lock.CustomInsertLockRecordGenerator;
-import org.keycloak.connections.jpa.updater.liquibase.lock.CustomLockDatabaseChangeLogGenerator;
-import org.keycloak.connections.jpa.updater.liquibase.lock.CustomLockService;
-import org.keycloak.connections.jpa.updater.liquibase.lock.DummyLockService;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.KeycloakSessionFactory;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -53,7 +52,7 @@ public class DefaultLiquibaseConnectionProvider implements LiquibaseConnectionPr
private static final Logger logger = Logger.getLogger(DefaultLiquibaseConnectionProvider.class);
private volatile boolean initialized = false;
-
+
@Override
public LiquibaseConnectionProvider create(KeycloakSession session) {
if (!initialized) {
@@ -132,9 +131,26 @@ public class DefaultLiquibaseConnectionProvider implements LiquibaseConnectionPr
}
String changelog = (database instanceof DB2Database) ? LiquibaseJpaUpdaterProvider.DB2_CHANGELOG : LiquibaseJpaUpdaterProvider.CHANGELOG;
- logger.debugf("Using changelog file: %s", changelog);
+ ResourceAccessor resourceAccessor = new ClassLoaderResourceAccessor(getClass().getClassLoader());
+
+ logger.debugf("Using changelog file %s and changelogTableName %s", changelog, database.getDatabaseChangeLogTableName());
+
+ return new Liquibase(changelog, resourceAccessor, database);
+ }
+
+ @Override
+ public Liquibase getLiquibaseForCustomUpdate(Connection connection, String defaultSchema, String changelogLocation, ClassLoader classloader, String changelogTableName) throws LiquibaseException {
+ Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(connection));
+ if (defaultSchema != null) {
+ database.setDefaultSchemaName(defaultSchema);
+ }
+
+ ResourceAccessor resourceAccessor = new ClassLoaderResourceAccessor(classloader);
+ database.setDatabaseChangeLogTableName(changelogTableName);
+
+ logger.debugf("Using changelog file %s and changelogTableName %s", changelogLocation, database.getDatabaseChangeLogTableName());
- return new Liquibase(changelog, new ClassLoaderResourceAccessor(getClass().getClassLoader()), database);
+ return new Liquibase(changelogLocation, resourceAccessor, database);
}
private static class LogWrapper extends LogFactory {
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/LiquibaseConnectionProvider.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/LiquibaseConnectionProvider.java
index 5aa81cc..215bd1d 100644
--- a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/LiquibaseConnectionProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/LiquibaseConnectionProvider.java
@@ -30,4 +30,6 @@ public interface LiquibaseConnectionProvider extends Provider {
Liquibase getLiquibase(Connection connection, String defaultSchema) throws LiquibaseException;
+ Liquibase getLiquibaseForCustomUpdate(Connection connection, String defaultSchema, String changelogLocation, ClassLoader classloader, String changelogTableName) throws LiquibaseException;
+
}
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java
index 8e9d253..2610174 100755
--- a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java
@@ -23,12 +23,17 @@ import liquibase.changelog.ChangeSet;
import liquibase.changelog.RanChangeSet;
import liquibase.exception.LiquibaseException;
import org.jboss.logging.Logger;
+import org.keycloak.common.util.reflections.Reflections;
+import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider;
import org.keycloak.connections.jpa.updater.JpaUpdaterProvider;
import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProvider;
+import org.keycloak.connections.jpa.util.JpaUtils;
import org.keycloak.models.KeycloakSession;
+import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.List;
+import java.util.Set;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -54,25 +59,20 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
ThreadLocalSessionContext.setCurrentSession(session);
try {
- Liquibase liquibase = getLiquibase(connection, defaultSchema);
-
- List<ChangeSet> changeSets = liquibase.listUnrunChangeSets((Contexts) null);
- if (!changeSets.isEmpty()) {
- if (changeSets.get(0).getId().equals(FIRST_VERSION)) {
- logger.info("Initializing database schema");
- } else {
- if (logger.isDebugEnabled()) {
- List<RanChangeSet> ranChangeSets = liquibase.getDatabase().getRanChangeSetList();
- logger.debugv("Updating database from {0} to {1}", ranChangeSets.get(ranChangeSets.size() - 1).getId(), changeSets.get(changeSets.size() - 1).getId());
- } else {
- logger.infov("Updating database");
- }
+ // Run update with keycloak master changelog first
+ Liquibase liquibase = getLiquibaseForKeycloakUpdate(connection, defaultSchema);
+ updateChangeSet(liquibase, liquibase.getChangeLogFile());
+
+ // Run update for each custom JpaEntityProvider
+ Set<JpaEntityProvider> jpaProviders = session.getAllProviders(JpaEntityProvider.class);
+ for (JpaEntityProvider jpaProvider : jpaProviders) {
+ String customChangelog = jpaProvider.getChangelogLocation();
+ if (customChangelog != null) {
+ String factoryId = jpaProvider.getFactoryId();
+ String changelogTableName = JpaUtils.getCustomChangelogTableName(factoryId);
+ liquibase = getLiquibaseForCustomProviderUpdate(connection, defaultSchema, customChangelog, jpaProvider.getClass().getClassLoader(), changelogTableName);
+ updateChangeSet(liquibase, liquibase.getChangeLogFile());
}
-
- liquibase.update((Contexts) null);
- logger.debug("Completed database update");
- } else {
- logger.debug("Database is up to date");
}
} catch (Exception e) {
throw new RuntimeException("Failed to update database", e);
@@ -81,21 +81,50 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
}
}
+ protected void updateChangeSet(Liquibase liquibase, String changelog) throws LiquibaseException {
+ List<ChangeSet> changeSets = liquibase.listUnrunChangeSets((Contexts) null);
+ if (!changeSets.isEmpty()) {
+ List<RanChangeSet> ranChangeSets = liquibase.getDatabase().getRanChangeSetList();
+ if (ranChangeSets.isEmpty()) {
+ logger.infov("Initializing database schema. Using changelog {0}", changelog);
+ } else {
+ if (logger.isDebugEnabled()) {
+ logger.debugv("Updating database from {0} to {1}. Using changelog {2}", ranChangeSets.get(ranChangeSets.size() - 1).getId(), changeSets.get(changeSets.size() - 1).getId(), changelog);
+ } else {
+ logger.infov("Updating database. Using changelog {0}", changelog);
+ }
+ }
+
+ liquibase.update((Contexts) null);
+ logger.debugv("Completed database update for changelog {0}", changelog);
+ } else {
+ logger.debugv("Database is up to date for changelog {0}", changelog);
+
+ // Needs to restart liquibase services to clear changeLogHistory.
+ Method resetServices = Reflections.findDeclaredMethod(Liquibase.class, "resetServices");
+ Reflections.invokeMethod(true, resetServices, liquibase);
+ }
+ }
+
@Override
public void validate(Connection connection, String defaultSchema) {
logger.debug("Validating if database is updated");
try {
- Liquibase liquibase = getLiquibase(connection, defaultSchema);
-
- List<ChangeSet> changeSets = liquibase.listUnrunChangeSets((Contexts) null);
- if (!changeSets.isEmpty()) {
- List<RanChangeSet> ranChangeSets = liquibase.getDatabase().getRanChangeSetList();
- String errorMessage = String.format("Failed to validate database schema. Schema needs updating database from %s to %s. Please change databaseSchema to 'update' or use other database",
- ranChangeSets.get(ranChangeSets.size() - 1).getId(), changeSets.get(changeSets.size() - 1).getId());
- throw new RuntimeException(errorMessage);
- } else {
- logger.debug("Validation passed. Database is up-to-date");
+ // Validate with keycloak master changelog first
+ Liquibase liquibase = getLiquibaseForKeycloakUpdate(connection, defaultSchema);
+ validateChangeSet(liquibase, liquibase.getChangeLogFile());
+
+ // Validate each custom JpaEntityProvider
+ Set<JpaEntityProvider> jpaProviders = session.getAllProviders(JpaEntityProvider.class);
+ for (JpaEntityProvider jpaProvider : jpaProviders) {
+ String customChangelog = jpaProvider.getChangelogLocation();
+ if (customChangelog != null) {
+ String factoryId = jpaProvider.getFactoryId();
+ String changelogTableName = JpaUtils.getCustomChangelogTableName(factoryId);
+ liquibase = getLiquibaseForCustomProviderUpdate(connection, defaultSchema, customChangelog, jpaProvider.getClass().getClassLoader(), changelogTableName);
+ validateChangeSet(liquibase, liquibase.getChangeLogFile());
+ }
}
} catch (LiquibaseException e) {
@@ -103,11 +132,28 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
}
}
- private Liquibase getLiquibase(Connection connection, String defaultSchema) throws LiquibaseException {
+ protected void validateChangeSet(Liquibase liquibase, String changelog) throws LiquibaseException {
+ List<ChangeSet> changeSets = liquibase.listUnrunChangeSets((Contexts) null);
+ if (!changeSets.isEmpty()) {
+ List<RanChangeSet> ranChangeSets = liquibase.getDatabase().getRanChangeSetList();
+ String errorMessage = String.format("Failed to validate database schema. Schema needs updating database from %s to %s. Please change databaseSchema to 'update' or use other database. Used changelog was %s",
+ ranChangeSets.get(ranChangeSets.size() - 1).getId(), changeSets.get(changeSets.size() - 1).getId(), changelog);
+ throw new RuntimeException(errorMessage);
+ } else {
+ logger.debugf("Validation passed. Database is up-to-date for changelog %s", changelog);
+ }
+ }
+
+ private Liquibase getLiquibaseForKeycloakUpdate(Connection connection, String defaultSchema) throws LiquibaseException {
LiquibaseConnectionProvider liquibaseProvider = session.getProvider(LiquibaseConnectionProvider.class);
return liquibaseProvider.getLiquibase(connection, defaultSchema);
}
+ private Liquibase getLiquibaseForCustomProviderUpdate(Connection connection, String defaultSchema, String changelogLocation, ClassLoader classloader, String changelogTableName) throws LiquibaseException {
+ LiquibaseConnectionProvider liquibaseProvider = session.getProvider(LiquibaseConnectionProvider.class);
+ return liquibaseProvider.getLiquibaseForCustomUpdate(connection, defaultSchema, changelogLocation, classloader, changelogTableName);
+ }
+
@Override
public void close() {
}
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/util/JpaUtils.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/util/JpaUtils.java
index de07fd5..5ac7d2f 100644
--- a/model/jpa/src/main/java/org/keycloak/connections/jpa/util/JpaUtils.java
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/util/JpaUtils.java
@@ -21,12 +21,18 @@ import org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl;
import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor;
import org.hibernate.jpa.boot.internal.PersistenceXmlParser;
import org.hibernate.jpa.boot.spi.Bootstrap;
+import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider;
+import org.keycloak.connections.jpa.entityprovider.ProxyClassLoader;
+import org.keycloak.models.KeycloakSession;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.spi.PersistenceUnitTransactionType;
+
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.Set;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -40,14 +46,52 @@ public class JpaUtils {
return (schema==null) ? tableName : schema + "." + tableName;
}
- public static EntityManagerFactory createEntityManagerFactory(String unitName, Map<String, Object> properties, ClassLoader classLoader) {
+ public static EntityManagerFactory createEntityManagerFactory(KeycloakSession session, String unitName, Map<String, Object> properties, ClassLoader classLoader) {
PersistenceXmlParser parser = new PersistenceXmlParser(new ClassLoaderServiceImpl(classLoader), PersistenceUnitTransactionType.RESOURCE_LOCAL);
List<ParsedPersistenceXmlDescriptor> persistenceUnits = parser.doResolve(properties);
for (ParsedPersistenceXmlDescriptor persistenceUnit : persistenceUnits) {
if (persistenceUnit.getName().equals(unitName)) {
- return Bootstrap.getEntityManagerFactoryBuilder(persistenceUnit, properties, classLoader).build();
+ List<Class<?>> providedEntities = getProvidedEntities(session);
+ for (Class<?> entityClass : providedEntities) {
+ // Add all extra entity classes to the persistence unit.
+ persistenceUnit.addClasses(entityClass.getName());
+ }
+ // Now build the entity manager factory, supplying a proxy classloader, so Hibernate will be able
+ // to find and load the extra provided entities. Set the provided classloader as parent classloader.
+ return Bootstrap.getEntityManagerFactoryBuilder(persistenceUnit, properties,
+ new ProxyClassLoader(providedEntities, classLoader)).build();
}
}
throw new RuntimeException("Persistence unit '" + unitName + "' not found");
}
+
+ /**
+ * Get a list of all provided entities by looping over all configured entity providers.
+ *
+ * @param session the keycloak session
+ * @return a list of all provided entities (can be an empty list)
+ */
+ public static List<Class<?>> getProvidedEntities(KeycloakSession session) {
+ List<Class<?>> providedEntityClasses = new ArrayList<>();
+ // Get all configured entity providers.
+ Set<JpaEntityProvider> entityProviders = session.getAllProviders(JpaEntityProvider.class);
+ // For every provider, add all entity classes to the list.
+ for (JpaEntityProvider entityProvider : entityProviders) {
+ providedEntityClasses.addAll(entityProvider.getEntities());
+ }
+ return providedEntityClasses;
+ }
+
+ /**
+ * Get the name of custom table for liquibase updates for give ID of JpaEntityProvider
+ * @param jpaEntityProviderFactoryId
+ * @return table name
+ */
+ public static String getCustomChangelogTableName(String jpaEntityProviderFactoryId) {
+ String upperCased = jpaEntityProviderFactoryId.toUpperCase();
+ upperCased = upperCased.replaceAll("-", "_");
+ upperCased = upperCased.replaceAll("[^A-Z_]", "");
+ return "DATABASECHANGELOG_" + upperCased.substring(0, Math.min(10, upperCased.length()));
+ }
+
}
diff --git a/model/jpa/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/model/jpa/src/main/resources/META-INF/services/org.keycloak.provider.Spi
index 94c6512..5aba7ba 100644
--- a/model/jpa/src/main/resources/META-INF/services/org.keycloak.provider.Spi
+++ b/model/jpa/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -17,4 +17,5 @@
org.keycloak.connections.jpa.JpaConnectionSpi
org.keycloak.connections.jpa.updater.JpaUpdaterSpi
-org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionSpi
\ No newline at end of file
+org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionSpi
+org.keycloak.connections.jpa.entityprovider.JpaEntitySpi
diff --git a/model/jpa/src/test/java/org/keycloak/connections/jpa/util/JpaUtilsTest.java b/model/jpa/src/test/java/org/keycloak/connections/jpa/util/JpaUtilsTest.java
new file mode 100644
index 0000000..838f1c0
--- /dev/null
+++ b/model/jpa/src/test/java/org/keycloak/connections/jpa/util/JpaUtilsTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.connections.jpa.util;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class JpaUtilsTest {
+
+ @Test
+ public void testConvertTableName() {
+ Assert.assertEquals("DATABASECHANGELOG_FOO", JpaUtils.getCustomChangelogTableName("foo"));
+ Assert.assertEquals("DATABASECHANGELOG_FOOBAR", JpaUtils.getCustomChangelogTableName("foo123bar"));
+ Assert.assertEquals("DATABASECHANGELOG_FOO_BAR", JpaUtils.getCustomChangelogTableName("foo_bar568"));
+ Assert.assertEquals("DATABASECHANGELOG_FOO_BAR_C", JpaUtils.getCustomChangelogTableName("foo-bar-c568"));
+ Assert.assertEquals("DATABASECHANGELOG_EXAMPLE_EN", JpaUtils.getCustomChangelogTableName("example-entity-provider"));
+ }
+}
diff --git a/server-spi/src/main/java/org/keycloak/provider/ProviderLoader.java b/server-spi/src/main/java/org/keycloak/provider/ProviderLoader.java
index 76c2950..2d7a07a 100644
--- a/server-spi/src/main/java/org/keycloak/provider/ProviderLoader.java
+++ b/server-spi/src/main/java/org/keycloak/provider/ProviderLoader.java
@@ -24,6 +24,19 @@ import java.util.List;
*/
public interface ProviderLoader {
+ /**
+ * Load the SPI definitions themselves.
+ *
+ * @return a list of Spi definition objects
+ */
+ List<Spi> loadSpis();
+
+ /**
+ * Load all provider factories of a specific SPI.
+ *
+ * @param spi the Spi definition
+ * @return a list of provider factories
+ */
List<ProviderFactory> load(Spi spi);
}
diff --git a/services/src/main/java/org/keycloak/provider/DefaultProviderLoader.java b/services/src/main/java/org/keycloak/provider/DefaultProviderLoader.java
index 26aa19d..5f4b19d 100644
--- a/services/src/main/java/org/keycloak/provider/DefaultProviderLoader.java
+++ b/services/src/main/java/org/keycloak/provider/DefaultProviderLoader.java
@@ -33,6 +33,15 @@ public class DefaultProviderLoader implements ProviderLoader {
}
@Override
+ public List<Spi> loadSpis() {
+ LinkedList<Spi> list = new LinkedList<>();
+ for (Spi spi : ServiceLoader.load(Spi.class, classLoader)) {
+ list.add(spi);
+ }
+ return list;
+ }
+
+ @Override
public List<ProviderFactory> load(Spi spi) {
LinkedList<ProviderFactory> list = new LinkedList<ProviderFactory>();
for (ProviderFactory f : ServiceLoader.load(spi.getProviderFactoryClass(), classLoader)) {
diff --git a/services/src/main/java/org/keycloak/provider/ProviderManager.java b/services/src/main/java/org/keycloak/provider/ProviderManager.java
index 997c68a..e906df9 100644
--- a/services/src/main/java/org/keycloak/provider/ProviderManager.java
+++ b/services/src/main/java/org/keycloak/provider/ProviderManager.java
@@ -65,6 +65,20 @@ public class ProviderManager {
}
}
+ public synchronized List<Spi> loadSpis() {
+ // Use a map to prevent duplicates, since the loaders may have overlapping classpaths.
+ Map<String, Spi> spiMap = new HashMap<>();
+ for (ProviderLoader loader : loaders) {
+ List<Spi> spis = loader.loadSpis();
+ if (spis != null) {
+ for (Spi spi : spis) {
+ spiMap.put(spi.getName(), spi);
+ }
+ }
+ }
+ return new LinkedList<>(spiMap.values());
+ }
+
public synchronized List<ProviderFactory> load(Spi spi) {
List<ProviderFactory> factories = cache.get(spi.getName());
if (factories == null) {
diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java
index 90b495c..172de6e 100755
--- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java
+++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java
@@ -70,8 +70,9 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory {
ProviderManager pm = new ProviderManager(getClass().getClassLoader(), Config.scope().getArray("providers"));
- ServiceLoader<Spi> load = ServiceLoader.load(Spi.class, getClass().getClassLoader());
- loadSPIs(pm, load);
+ // Load the SPI classes through the provider manager, so both Keycloak internal SPI's and
+ // the ones defined in deployed modules will be found.
+ loadSPIs(pm, pm.loadSpis());
for ( Map<String, ProviderFactory> factories : factoriesMap.values()) {
for (ProviderFactory factory : factories.values()) {
factory.postInit(this);
@@ -79,8 +80,8 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory {
}
}
- protected void loadSPIs(ProviderManager pm, ServiceLoader<Spi> load) {
- for (Spi spi : load) {
+ protected void loadSPIs(ProviderManager pm, List<Spi> spiList) {
+ for (Spi spi : spiList) {
spis.add(spi);
Map<String, ProviderFactory> factories = new HashMap<String, ProviderFactory>();
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
index b26d95a..1eee46b 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
@@ -496,9 +496,9 @@ public class RealmAdminResource {
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public List<EventRepresentation> getEvents(@QueryParam("type") List<String> types, @QueryParam("client") String client,
- @QueryParam("user") String user, @QueryParam("dateFrom") String dateFrom, @QueryParam("dateTo") String dateTo,
- @QueryParam("ipAddress") String ipAddress, @QueryParam("first") Integer firstResult,
- @QueryParam("max") Integer maxResults) {
+ @QueryParam("user") String user, @QueryParam("dateFrom") String dateFrom, @QueryParam("dateTo") String dateTo,
+ @QueryParam("ipAddress") String ipAddress, @QueryParam("first") Integer firstResult,
+ @QueryParam("max") Integer maxResults) {
auth.init(RealmAuth.Resource.EVENTS).requireView();
EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
@@ -585,10 +585,10 @@ public class RealmAdminResource {
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public List<AdminEventRepresentation> getEvents(@QueryParam("operationTypes") List<String> operationTypes, @QueryParam("authRealm") String authRealm, @QueryParam("authClient") String authClient,
- @QueryParam("authUser") String authUser, @QueryParam("authIpAddress") String authIpAddress,
- @QueryParam("resourcePath") String resourcePath, @QueryParam("dateFrom") String dateFrom,
- @QueryParam("dateTo") String dateTo, @QueryParam("first") Integer firstResult,
- @QueryParam("max") Integer maxResults) {
+ @QueryParam("authUser") String authUser, @QueryParam("authIpAddress") String authIpAddress,
+ @QueryParam("resourcePath") String resourcePath, @QueryParam("dateFrom") String dateFrom,
+ @QueryParam("dateTo") String dateTo, @QueryParam("first") Integer firstResult,
+ @QueryParam("max") Integer maxResults) {
auth.init(RealmAuth.Resource.EVENTS).requireView();
EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
diff --git a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
index 4161763..53c9aaf 100755
--- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
@@ -101,7 +101,7 @@ public class RealmsResource {
@Path("{realm}/protocol/{protocol}")
public Object getProtocol(final @PathParam("realm") String name,
- final @PathParam("protocol") String protocol) {
+ final @PathParam("protocol") String protocol) {
RealmModel realm = init(name);
LoginProtocolFactory factory = (LoginProtocolFactory)session.getKeycloakSessionFactory().getProviderFactory(LoginProtocol.class, protocol);
@@ -239,7 +239,7 @@ public class RealmsResource {
@Path("{realm}/.well-known/{provider}")
@Produces(MediaType.APPLICATION_JSON)
public Response getWellKnown(final @PathParam("realm") String name,
- final @PathParam("provider") String providerName) {
+ final @PathParam("provider") String providerName) {
init(name);
WellKnownProvider wellKnown = session.getProvider(WellKnownProvider.class, providerName);
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi
index 50bb346..55b31a0 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi
+++ b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -18,3 +18,4 @@
org.keycloak.exportimport.ClientDescriptionConverterSpi
org.keycloak.wellknown.WellKnownSpi
org.keycloak.services.clientregistration.ClientRegistrationSpi
+
testsuite/integration/pom.xml 4(+4 -0)
diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml
index d394e2a..c1d0412 100755
--- a/testsuite/integration/pom.xml
+++ b/testsuite/integration/pom.xml
@@ -140,6 +140,10 @@
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-client</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
<artifactId>keycloak-saml-servlet-filter-adapter</artifactId>
</dependency>
<dependency>
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/CompanyRepresentation.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/CompanyRepresentation.java
new file mode 100644
index 0000000..117b928
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/CompanyRepresentation.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.domainextension;
+
+
+import org.keycloak.testsuite.domainextension.jpa.Company;
+
+public class CompanyRepresentation {
+
+ private String id;
+ private String name;
+
+ public CompanyRepresentation() {
+ }
+
+ public CompanyRepresentation(Company company) {
+ id = company.getId();
+ name = company.getName();
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/jpa/Company.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/jpa/Company.java
new file mode 100644
index 0000000..d5f95ae
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/jpa/Company.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.domainextension.jpa;
+
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.Table;
+
+@Entity
+@Table(name = "EXAMPLE_COMPANY")
+@NamedQueries({ @NamedQuery(name = "findByRealm", query = "from Company where realmId = :realmId") })
+public class Company {
+
+ @Id
+ @Column(name = "ID")
+ private String id;
+
+ @Column(name = "NAME", nullable = false)
+ private String name;
+
+ @Column(name = "REALM_ID", nullable = false)
+ private String realmId;
+
+ public String getId() {
+ return id;
+ }
+
+ public String getRealmId() {
+ return realmId;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public void setRealmId(String realmId) {
+ this.realmId = realmId;
+ }
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/jpa/ExampleJpaEntityProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/jpa/ExampleJpaEntityProvider.java
new file mode 100644
index 0000000..2f87632
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/jpa/ExampleJpaEntityProvider.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.domainextension.jpa;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider;
+
+/**
+ * @author <a href="mailto:erik.mulder@docdatapayments.com">Erik Mulder</a>
+ *
+ * Example JpaEntityProvider.
+ */
+public class ExampleJpaEntityProvider implements JpaEntityProvider {
+
+ @Override
+ public List<Class<?>> getEntities() {
+ return Collections.<Class<?>>singletonList(Company.class);
+ }
+
+ @Override
+ public String getChangelogLocation() {
+ return "META-INF/example-changelog.xml";
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public String getFactoryId() {
+ return ExampleJpaEntityProviderFactory.ID;
+ }
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/jpa/ExampleJpaEntityProviderFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/jpa/ExampleJpaEntityProviderFactory.java
new file mode 100644
index 0000000..83dc0a7
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/jpa/ExampleJpaEntityProviderFactory.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.domainextension.jpa;
+
+import org.keycloak.Config.Scope;
+import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider;
+import org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
+/**
+ * @author <a href="mailto:erik.mulder@docdatapayments.com">Erik Mulder</a>
+ *
+ * Example JpaEntityProviderFactory.
+ */
+public class ExampleJpaEntityProviderFactory implements JpaEntityProviderFactory {
+
+ protected static final String ID = "example-entity-provider";
+
+ @Override
+ public JpaEntityProvider create(KeycloakSession session) {
+ return new ExampleJpaEntityProvider();
+ }
+
+ @Override
+ public String getId() {
+ return ID;
+ }
+
+ @Override
+ public void init(Scope config) {
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ }
+
+ @Override
+ public void close() {
+ }
+
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/rest/CompanyResource.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/rest/CompanyResource.java
new file mode 100644
index 0000000..197cbce
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/rest/CompanyResource.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.domainextension.rest;
+
+import java.util.List;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.testsuite.domainextension.CompanyRepresentation;
+import org.keycloak.testsuite.domainextension.spi.ExampleService;
+
+public class CompanyResource {
+
+ private final KeycloakSession session;
+
+ public CompanyResource(KeycloakSession session) {
+ this.session = session;
+ }
+
+ @GET
+ @Path("")
+ @NoCache
+ @Produces(MediaType.APPLICATION_JSON)
+ public List<CompanyRepresentation> getCompanies() {
+ return session.getProvider(ExampleService.class).listCompanies();
+ }
+
+ @DELETE
+ @Path("")
+ @NoCache
+ public void deleteAllCompanies() {
+ session.getProvider(ExampleService.class).deleteAllCompanies();
+ }
+
+ @POST
+ @Path("")
+ @NoCache
+ @Consumes(MediaType.APPLICATION_JSON)
+ public Response createCompany(CompanyRepresentation rep) {
+ session.getProvider(ExampleService.class).addCompany(rep);
+ return Response.created(session.getContext().getUri().getAbsolutePathBuilder().path(rep.getId()).build()).build();
+ }
+
+ @GET
+ @NoCache
+ @Path("{id}")
+ @Produces(MediaType.APPLICATION_JSON)
+ public CompanyRepresentation getCompany(@PathParam("id") final String id) {
+ return session.getProvider(ExampleService.class).findCompany(id);
+ }
+
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/rest/ExampleRealmResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/rest/ExampleRealmResourceProvider.java
new file mode 100644
index 0000000..ff9ad02
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/rest/ExampleRealmResourceProvider.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.domainextension.rest;
+
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.services.resource.RealmResourceProvider;
+
+public class ExampleRealmResourceProvider implements RealmResourceProvider {
+
+ private KeycloakSession session;
+
+ public ExampleRealmResourceProvider(KeycloakSession session) {
+ this.session = session;
+ }
+
+ @Override
+ public Object getResource() {
+ return new ExampleRestResource(session);
+ }
+
+ @Override
+ public void close() {
+ }
+
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/rest/ExampleRealmResourceProviderFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/rest/ExampleRealmResourceProviderFactory.java
new file mode 100644
index 0000000..ae016b6
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/rest/ExampleRealmResourceProviderFactory.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.domainextension.rest;
+
+import org.keycloak.Config.Scope;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.services.resource.RealmResourceProvider;
+import org.keycloak.services.resource.RealmResourceProviderFactory;
+
+public class ExampleRealmResourceProviderFactory implements RealmResourceProviderFactory {
+
+ public static final String ID = "example";
+
+ @Override
+ public String getId() {
+ return ID;
+ }
+
+ @Override
+ public RealmResourceProvider create(KeycloakSession session) {
+ return new ExampleRealmResourceProvider(session);
+ }
+
+ @Override
+ public void init(Scope config) {
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ }
+
+ @Override
+ public void close() {
+ }
+
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/rest/ExampleRestResource.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/rest/ExampleRestResource.java
new file mode 100644
index 0000000..629acc8
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/rest/ExampleRestResource.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.domainextension.rest;
+
+import javax.ws.rs.ForbiddenException;
+import javax.ws.rs.NotAuthorizedException;
+import javax.ws.rs.Path;
+
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.services.managers.AppAuthManager;
+import org.keycloak.services.managers.AuthenticationManager;
+
+public class ExampleRestResource {
+
+ private final KeycloakSession session;
+ private final AuthenticationManager.AuthResult auth;
+
+ public ExampleRestResource(KeycloakSession session) {
+ this.session = session;
+ this.auth = new AppAuthManager().authenticateBearerToken(session, session.getContext().getRealm());
+ }
+
+ @Path("companies")
+ public CompanyResource getCompanyResource() {
+ return new CompanyResource(session);
+ }
+
+ // Same like "companies" endpoint, but REST endpoint is authenticated with Bearer token and user must be in realm role "admin"
+ // Just for illustration purposes
+ @Path("companies-auth")
+ public CompanyResource getCompanyResourceAuthenticated() {
+ checkRealmAdmin();
+ return new CompanyResource(session);
+ }
+
+ private void checkRealmAdmin() {
+ if (auth == null) {
+ throw new NotAuthorizedException("Bearer");
+ } else if (auth.getToken().getRealmAccess() == null || !auth.getToken().getRealmAccess().isUserInRole("admin")) {
+ throw new ForbiddenException("Does not have realm admin role");
+ }
+ }
+
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/ExampleService.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/ExampleService.java
new file mode 100644
index 0000000..dd47d96
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/ExampleService.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.domainextension.spi;
+
+import java.util.List;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.testsuite.domainextension.CompanyRepresentation;
+
+public interface ExampleService extends Provider {
+
+ List<CompanyRepresentation> listCompanies();
+
+ CompanyRepresentation findCompany(String id);
+
+ CompanyRepresentation addCompany(CompanyRepresentation company);
+
+ void deleteAllCompanies();
+
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/ExampleServiceProviderFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/ExampleServiceProviderFactory.java
new file mode 100644
index 0000000..ee802b0
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/ExampleServiceProviderFactory.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.domainextension.spi;
+
+import org.keycloak.provider.ProviderFactory;
+
+public interface ExampleServiceProviderFactory extends ProviderFactory<ExampleService> {
+
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/ExampleSpi.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/ExampleSpi.java
new file mode 100644
index 0000000..d85f043
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/ExampleSpi.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.domainextension.spi;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+public class ExampleSpi implements Spi {
+
+ @Override
+ public boolean isInternal() {
+ return false;
+ }
+
+ @Override
+ public String getName() {
+ return "example";
+ }
+
+ @Override
+ public Class<? extends Provider> getProviderClass() {
+ return ExampleService.class;
+ }
+
+ @Override
+ @SuppressWarnings("rawtypes")
+ public Class<? extends ProviderFactory> getProviderFactoryClass() {
+ return ExampleServiceProviderFactory.class;
+ }
+
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/impl/ExampleServiceImpl.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/impl/ExampleServiceImpl.java
new file mode 100644
index 0000000..213f7e5
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/impl/ExampleServiceImpl.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.domainextension.spi.impl;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.persistence.EntityManager;
+
+import org.keycloak.connections.jpa.JpaConnectionProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.testsuite.domainextension.CompanyRepresentation;
+import org.keycloak.testsuite.domainextension.jpa.Company;
+import org.keycloak.testsuite.domainextension.spi.ExampleService;
+
+public class ExampleServiceImpl implements ExampleService {
+
+ private final KeycloakSession session;
+
+ public ExampleServiceImpl(KeycloakSession session) {
+ this.session = session;
+ if (getRealm() == null) {
+ throw new IllegalStateException("The service cannot accept a session without a realm in it's context.");
+ }
+ }
+
+ private EntityManager getEntityManager() {
+ return session.getProvider(JpaConnectionProvider.class).getEntityManager();
+ }
+
+ protected RealmModel getRealm() {
+ return session.getContext().getRealm();
+ }
+
+ @Override
+ public List<CompanyRepresentation> listCompanies() {
+ List<Company> companyEntities = getEntityManager().createNamedQuery("findByRealm", Company.class)
+ .setParameter("realmId", getRealm().getId())
+ .getResultList();
+
+ List<CompanyRepresentation> result = new LinkedList<>();
+ for (Company entity : companyEntities) {
+ result.add(new CompanyRepresentation(entity));
+ }
+ return result;
+ }
+
+ @Override
+ public CompanyRepresentation findCompany(String id) {
+ Company entity = getEntityManager().find(Company.class, id);
+ return entity==null ? null : new CompanyRepresentation(entity);
+ }
+
+ @Override
+ public CompanyRepresentation addCompany(CompanyRepresentation company) {
+ Company entity = new Company();
+ String id = company.getId()==null ? KeycloakModelUtils.generateId() : company.getId();
+ entity.setId(id);
+ entity.setName(company.getName());
+ entity.setRealmId(getRealm().getId());
+ getEntityManager().persist(entity);
+
+ company.setId(id);
+ return company;
+ }
+
+ @Override
+ public void deleteAllCompanies() {
+ EntityManager em = getEntityManager();
+ List<Company> companyEntities = em.createNamedQuery("findByRealm", Company.class)
+ .setParameter("realmId", getRealm().getId())
+ .getResultList();
+
+ for (Company entity : companyEntities) {
+ em.remove(entity);
+ }
+ }
+
+ public void close() {
+ // Nothing to do.
+ }
+
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/impl/ExampleServiceProviderFactoryImpl.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/impl/ExampleServiceProviderFactoryImpl.java
new file mode 100644
index 0000000..c772827
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/domainextension/spi/impl/ExampleServiceProviderFactoryImpl.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.domainextension.spi.impl;
+
+import org.keycloak.Config.Scope;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.testsuite.domainextension.spi.ExampleService;
+import org.keycloak.testsuite.domainextension.spi.ExampleServiceProviderFactory;
+
+public class ExampleServiceProviderFactoryImpl implements ExampleServiceProviderFactory {
+
+ @Override
+ public ExampleService create(KeycloakSession session) {
+ return new ExampleServiceImpl(session);
+ }
+
+ @Override
+ public void init(Scope config) {
+
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @Override
+ public String getId() {
+ return "exampleServiceImpl";
+ }
+
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/example-changelog.xml b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/example-changelog.xml
new file mode 100644
index 0000000..5edd719
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/example-changelog.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
+ <changeSet author="erik.mulder@docdatapayments.com" id="example-1.0">
+
+ <createTable tableName="EXAMPLE_COMPANY">
+ <column name="ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="NAME" type="VARCHAR(255)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="REALM_ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ </createTable>
+
+ <addPrimaryKey
+ constraintName="PK_COMPANY"
+ tableName="EXAMPLE_COMPANY"
+ columnNames="ID"
+ />
+
+ </changeSet>
+
+</databaseChangeLog>
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory
new file mode 100644
index 0000000..67464bd
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory
@@ -0,0 +1,18 @@
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.keycloak.testsuite.domainextension.jpa.ExampleJpaEntityProviderFactory
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.provider.Spi
new file mode 100644
index 0000000..1c31c00
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -0,0 +1,18 @@
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.keycloak.testsuite.domainextension.spi.ExampleSpi
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory
index 8823682..1958ff0 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory
@@ -16,4 +16,5 @@
#
org.keycloak.testsuite.rest.TestingResourceProviderFactory
-org.keycloak.testsuite.rest.TestApplicationResourceProviderFactory
\ No newline at end of file
+org.keycloak.testsuite.rest.TestApplicationResourceProviderFactory
+org.keycloak.testsuite.domainextension.rest.ExampleRealmResourceProviderFactory
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.testsuite.domainextension.spi.ExampleServiceProviderFactory b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.testsuite.domainextension.spi.ExampleServiceProviderFactory
new file mode 100644
index 0000000..d58236f
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.testsuite.domainextension.spi.ExampleServiceProviderFactory
@@ -0,0 +1,18 @@
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.keycloak.testsuite.domainextension.spi.impl.ExampleServiceProviderFactoryImpl
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/org/keycloak/testsuite/integration-arquillian-testsuite-providers/main/module.xml b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/org/keycloak/testsuite/integration-arquillian-testsuite-providers/main/module.xml
index a5d327f..28e4789 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/org/keycloak/testsuite/integration-arquillian-testsuite-providers/main/module.xml
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/org/keycloak/testsuite/integration-arquillian-testsuite-providers/main/module.xml
@@ -30,8 +30,12 @@
<module name="org.keycloak.keycloak-server-spi"/>
<module name="org.keycloak.keycloak-services"/>
<module name="org.keycloak.keycloak-model-infinispan"/>
+ <module name="org.keycloak.keycloak-model-jpa"/>
<module name="org.infinispan"/>
<module name="org.jboss.logging"/>
- <module name="org.jboss.resteasy.resteasy-jaxrs"/>
+ <module name="org.jboss.resteasy.resteasy-jaxrs"/>
+ <module name="javax.persistence.api"/>
+ <module name="org.hibernate"/>
+ <module name="org.javassist"/>
</dependencies>
</module>
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/KeycloakTestingClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/KeycloakTestingClient.java
index 46f7b23..dedc08d 100755
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/KeycloakTestingClient.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/KeycloakTestingClient.java
@@ -21,6 +21,7 @@ import org.jboss.resteasy.client.jaxrs.ResteasyClient;
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget;
import org.keycloak.testsuite.client.resources.TestApplicationResource;
+import org.keycloak.testsuite.client.resources.TestExampleCompanyResource;
import org.keycloak.testsuite.client.resources.TestingResource;
/**
@@ -45,6 +46,8 @@ public class KeycloakTestingClient {
public TestApplicationResource testApp() { return target.proxy(TestApplicationResource.class); }
+ public TestExampleCompanyResource testExampleCompany() { return target.proxy(TestExampleCompanyResource.class); }
+
public void close() {
client.close();
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestExampleCompanyResource.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestExampleCompanyResource.java
new file mode 100644
index 0000000..00d6e28
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/resources/TestExampleCompanyResource.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.client.resources;
+
+import java.util.List;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.keycloak.testsuite.domainextension.CompanyRepresentation;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@Path("/realms/{realmName}/example/companies")
+@Consumes(MediaType.APPLICATION_JSON)
+public interface TestExampleCompanyResource {
+
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ List<CompanyRepresentation> getCompanies(@PathParam("realmName") String realmName);
+
+ @GET
+ @Path("/{companyId}")
+ @Produces(MediaType.APPLICATION_JSON)
+ CompanyRepresentation getCompany(@PathParam("realmName") String realmName, @PathParam("companyId") String companyId);
+
+ @POST
+ @Consumes(MediaType.APPLICATION_JSON)
+ Response createCompany(@PathParam("realmName") String realmName, CompanyRepresentation rep);
+
+ @DELETE
+ void deleteAllCompanies(@PathParam("realmName") String realmName);
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/domainextension/CustomExtensionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/domainextension/CustomExtensionTest.java
new file mode 100644
index 0000000..36b5702
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/domainextension/CustomExtensionTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.domainextension;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.junit.Test;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.client.resources.TestExampleCompanyResource;
+import org.keycloak.testsuite.util.RealmBuilder;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class CustomExtensionTest extends AbstractKeycloakTest {
+
+ @Override
+ public void addTestRealms(List<RealmRepresentation> testRealms) {
+ RealmRepresentation foo = RealmBuilder.create().name("foo").build();
+ testRealms.add(foo);
+ }
+
+ @Test
+ public void testDomainExtension() throws Exception {
+ companyResource().createCompany("foo", buildCompany("foo-company"));
+ companyResource().createCompany("foo", buildCompany("bar-company"));
+ companyResource().createCompany("master", buildCompany("master-company"));
+
+ List<CompanyRepresentation> fooCompanies = companyResource().getCompanies("foo");
+ List<CompanyRepresentation> masterCompanies = companyResource().getCompanies("master");
+
+ assertCompanyNames(fooCompanies, "foo-company", "bar-company");
+ assertCompanyNames(masterCompanies, "master-company");
+
+ companyResource().deleteAllCompanies("foo");
+ companyResource().deleteAllCompanies("master");
+ }
+
+ private TestExampleCompanyResource companyResource() {
+ return testingClient.testExampleCompany();
+ }
+
+ private CompanyRepresentation buildCompany(String companyName) {
+ CompanyRepresentation rep = new CompanyRepresentation();
+ rep.setName(companyName);
+ return rep;
+ }
+
+ private void assertCompanyNames(List<CompanyRepresentation> companies, String... expectedNames) {
+ Set<String> names = new HashSet<>();
+ for (CompanyRepresentation comp : companies) {
+ names.add(comp.getName());
+ }
+
+ Assert.assertEquals(expectedNames.length, names.size());
+ for (String expectedName : expectedNames) {
+ Assert.assertTrue(names.contains(expectedName));
+ }
+ }
+}