diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/DefaultLiquibaseConnectionProvider.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/DefaultLiquibaseConnectionProvider.java new file mode 100644 index 0000000000..8b66d27b3f --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/DefaultLiquibaseConnectionProvider.java @@ -0,0 +1,105 @@ +/* + * Copyright 2021 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.models.map.storage.jpa.liquibase; + +import java.sql.Connection; +import java.util.concurrent.atomic.AtomicBoolean; + +import liquibase.Liquibase; +import liquibase.change.ChangeFactory; +import liquibase.database.Database; +import liquibase.database.DatabaseFactory; +import liquibase.database.jvm.JdbcConnection; +import liquibase.datatype.DataTypeFactory; +import liquibase.exception.LiquibaseException; +import liquibase.resource.ClassLoaderResourceAccessor; +import liquibase.resource.ResourceAccessor; +import liquibase.sqlgenerator.SqlGeneratorFactory; +import org.jboss.logging.Logger; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.map.storage.jpa.liquibase.extension.GeneratedColumnSqlGenerator; +import org.keycloak.models.map.storage.jpa.liquibase.extension.CreateJsonIndexChange; +import org.keycloak.models.map.storage.jpa.liquibase.extension.CreateJsonIndexGenerator; +import org.keycloak.models.map.storage.jpa.liquibase.extension.GeneratedColumnChange; +import org.keycloak.models.map.storage.jpa.liquibase.extension.JsonDataType; + +/** + * A {@link MapLiquibaseConnectionProvider} implementation for the map-jpa module. This provider registers the custom {@code Liquibase} + * changes and data types that were developed to better support working with data stored as JSON in the database. + *

+ * An instance of this provider can be obtained via {@link KeycloakSession#getProvider(Class)} as follows: + *
+ *     MapLiquibaseConnectionProvider liquibaseProvider = session.getProvider(MapLiquibaseConnectionProvider.class);
+ * 
+ * + * @author Stefan Guilhen + */ +public class DefaultLiquibaseConnectionProvider implements MapLiquibaseConnectionProvider { + + private static final Logger logger = Logger.getLogger(DefaultLiquibaseConnectionProvider.class); + + private static final AtomicBoolean INITIALIZED = new AtomicBoolean(false); + + public DefaultLiquibaseConnectionProvider(final KeycloakSession session) { + if (! INITIALIZED.get()) { + // TODO: all liquibase providers should probably synchronize on the same object. + synchronized (INITIALIZED) { + if (! INITIALIZED.get()) { + initializeLiquibase(); + INITIALIZED.set(true); + } + } + } + } + + /** + * Registers the custom changes/types so we can work with data stored in JSON format. + */ + protected void initializeLiquibase() { + + // Add custom JSON data type + DataTypeFactory.getInstance().register(JsonDataType.class); + + // Add custom change to generate columns from properties in JSON files stored in the DB. + ChangeFactory.getInstance().register(GeneratedColumnChange.class); + SqlGeneratorFactory.getInstance().register(new GeneratedColumnSqlGenerator()); + + // Add custom change to create indexes for properties in JSON files stored in the DB. + ChangeFactory.getInstance().register(CreateJsonIndexChange.class); + SqlGeneratorFactory.getInstance().register(new CreateJsonIndexGenerator()); + } + + @Override + public void close() { + } + + @Override + public Liquibase getLiquibaseForCustomUpdate(final Connection connection, final String defaultSchema, final String changelogLocation, + final ClassLoader classloader, final 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(changelogLocation, resourceAccessor, database); + } +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/DefaultLiquibaseConnectionProviderFactory.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/DefaultLiquibaseConnectionProviderFactory.java new file mode 100644 index 0000000000..ea6b1f84a3 --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/DefaultLiquibaseConnectionProviderFactory.java @@ -0,0 +1,55 @@ +/* + * Copyright 2021 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.models.map.storage.jpa.liquibase; + +import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; + +/** + * {@link MapLiquibaseConnectionProviderFactory} implementation for the map-jpa module. It produces an instance of + * {@link DefaultLiquibaseConnectionProvider} when {@link #create(KeycloakSession)} is called. + * + * @author Stefan Guilhen + */ +public class DefaultLiquibaseConnectionProviderFactory implements MapLiquibaseConnectionProviderFactory { + + public static final String PROVIDER_ID = "default"; + + @Override + public MapLiquibaseConnectionProvider create(KeycloakSession session) { + return new DefaultLiquibaseConnectionProvider(session); + } + + @Override + public void init(Config.Scope config) { + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public void close() { + } + + @Override + public String getId() { + return PROVIDER_ID; + } +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/updater/liquibase/MapJpaLiquibaseUpdaterProvider.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/MapJpaLiquibaseUpdaterProvider.java similarity index 93% rename from model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/updater/liquibase/MapJpaLiquibaseUpdaterProvider.java rename to model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/MapJpaLiquibaseUpdaterProvider.java index 743a39456d..04e3044b51 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/updater/liquibase/MapJpaLiquibaseUpdaterProvider.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/MapJpaLiquibaseUpdaterProvider.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.keycloak.models.map.storage.jpa.updater.liquibase; +package org.keycloak.models.map.storage.jpa.liquibase; import java.io.File; import java.io.FileWriter; @@ -33,12 +33,10 @@ 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.updater.liquibase.conn.LiquibaseConnectionProvider; import org.keycloak.connections.jpa.updater.liquibase.ThreadLocalSessionContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.map.storage.ModelEntityUtil; import org.keycloak.models.map.storage.jpa.updater.MapJpaUpdaterProvider; -import org.keycloak.models.map.storage.jpa.updater.MapJpaUpdaterProvider.Status; public class MapJpaLiquibaseUpdaterProvider implements MapJpaUpdaterProvider { @@ -163,12 +161,12 @@ public class MapJpaLiquibaseUpdaterProvider implements MapJpaUpdaterProvider { } private Liquibase getLiquibase(Class modelType, Connection connection, String defaultSchema) throws LiquibaseException { - LiquibaseConnectionProvider liquibaseProvider = session.getProvider(LiquibaseConnectionProvider.class); - String changelog = "META-INF/jpa-" + ModelEntityUtil.getModelName(modelType) + "-changelog.xml"; - if (changelog == null) { + MapLiquibaseConnectionProvider liquibaseProvider = session.getProvider(MapLiquibaseConnectionProvider.class); + String modelName = ModelEntityUtil.getModelName(modelType); + if (modelName == null) { throw new IllegalStateException("Cannot find changlelog for modelClass " + modelType.getName()); } - + String changelog = "META-INF/jpa-" + modelName + "-changelog.xml"; return liquibaseProvider.getLiquibaseForCustomUpdate(connection, defaultSchema, changelog, this.getClass().getClassLoader(), "databasechangelog"); } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/updater/liquibase/MapJpaLiquibaseUpdaterProviderFactory.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/MapJpaLiquibaseUpdaterProviderFactory.java similarity index 96% rename from model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/updater/liquibase/MapJpaLiquibaseUpdaterProviderFactory.java rename to model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/MapJpaLiquibaseUpdaterProviderFactory.java index ca79063c3a..0e1f95cb4e 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/updater/liquibase/MapJpaLiquibaseUpdaterProviderFactory.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/MapJpaLiquibaseUpdaterProviderFactory.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.keycloak.models.map.storage.jpa.updater.liquibase; +package org.keycloak.models.map.storage.jpa.liquibase; import org.keycloak.Config; import org.keycloak.models.KeycloakSession; diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/MapLiquibaseConnectionProvider.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/MapLiquibaseConnectionProvider.java new file mode 100644 index 0000000000..0c94c05a9e --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/MapLiquibaseConnectionProvider.java @@ -0,0 +1,29 @@ +/* + * Copyright 2021 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.models.map.storage.jpa.liquibase; + +import java.sql.Connection; + +import liquibase.Liquibase; +import liquibase.exception.LiquibaseException; +import org.keycloak.provider.Provider; + +public interface MapLiquibaseConnectionProvider extends Provider { + + Liquibase getLiquibaseForCustomUpdate(Connection connection, String defaultSchema, String changelogLocation, ClassLoader classloader, String changelogTableName) throws LiquibaseException; +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/MapLiquibaseConnectionProviderFactory.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/MapLiquibaseConnectionProviderFactory.java new file mode 100644 index 0000000000..f813eb2c2d --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/MapLiquibaseConnectionProviderFactory.java @@ -0,0 +1,23 @@ +/* + * Copyright 2021 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.models.map.storage.jpa.liquibase; + +import org.keycloak.provider.ProviderFactory; + +public interface MapLiquibaseConnectionProviderFactory extends ProviderFactory { +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/MapLiquibaseConnectionSpi.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/MapLiquibaseConnectionSpi.java new file mode 100644 index 0000000000..621ab6e0ed --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/MapLiquibaseConnectionSpi.java @@ -0,0 +1,47 @@ +/* + * Copyright 2021 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.models.map.storage.jpa.liquibase; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +public class MapLiquibaseConnectionSpi implements Spi { + + public final static String SPI_NAME = "mapLiquibaseConnection"; + + @Override + public boolean isInternal() { + return true; + } + + @Override + public String getName() { + return SPI_NAME; + } + + @Override + public Class getProviderClass() { + return MapLiquibaseConnectionProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return MapLiquibaseConnectionProviderFactory.class; + } +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/extension/CreateJsonIndexChange.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/extension/CreateJsonIndexChange.java new file mode 100644 index 0000000000..a98e878b4c --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/extension/CreateJsonIndexChange.java @@ -0,0 +1,194 @@ +/* + * Copyright 2021 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.models.map.storage.jpa.liquibase.extension; + +import java.util.List; +import java.util.stream.Collectors; + +import liquibase.change.AbstractChange; +import liquibase.change.Change; +import liquibase.change.ChangeMetaData; +import liquibase.change.ChangeStatus; +import liquibase.change.ChangeWithColumns; +import liquibase.change.DatabaseChange; +import liquibase.change.DatabaseChangeProperty; +import liquibase.change.core.CreateIndexChange; +import liquibase.change.core.DropIndexChange; +import liquibase.database.Database; +import liquibase.statement.SqlStatement; + +/** + * Extension used to create an index for properties of JSON files stored in the database. Some databases, like {@code Postgres}, + * have native support for these indexes while other databases may require different constructs to achieve this (like creation + * of a separate column based on the JSON property and subsequent indexing of that column). + *

+ * Example configuration in the changelog: + *

+ *     <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ *                    xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
+ *                    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
+ *                    http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd
+ *                    http://www.liquibase.org/xml/ns/dbchangelog-ext
+ *                    http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
+ *
+ *     <changeSet author="keycloak" id="some_id">
+ *         ...
+ *         <ext:createJsonIndex tableName="test" indexName="some_index_name">
+ *             <ext:column jsonColumn="metadata" jsonProperty="name"/>
+ *         </ext:createJsonIndex>
+ *     </changeSet>
+ * 
+ * The above configuration is creating an index for the {@code name} property of JSON files stored in column {@code metadata} in + * table {@code test}. + * + * @author Stefan Guilhen + */ +@DatabaseChange(name="createJsonIndex", description = "Creates an index for one or more JSON properties", + priority = ChangeMetaData.PRIORITY_DEFAULT, appliesTo = "index") +public class CreateJsonIndexChange extends AbstractChange implements ChangeWithColumns { + + private final CreateIndexChange delegate; + + public CreateJsonIndexChange() { + this.delegate = new CreateIndexChange(); + } + + @DatabaseChangeProperty + public String getCatalogName() { + return delegate.getCatalogName(); + } + + public void setCatalogName(String catalogName) { + this.delegate.setCatalogName(catalogName); + } + + @DatabaseChangeProperty(mustEqualExisting ="index.schema") + public String getSchemaName() { + return delegate.getSchemaName(); + } + + public void setSchemaName(String schemaName) { + this.delegate.setSchemaName(schemaName); + } + + @DatabaseChangeProperty(mustEqualExisting = "index.table", description = "Name of the table to add the index to", exampleValue = "person") + public String getTableName() { + return this.delegate.getTableName(); + } + + public void setTableName(String tableName) { + this.delegate.setTableName(tableName); + } + + @DatabaseChangeProperty(mustEqualExisting = "index", description = "Name of the index to create") + public String getIndexName() { + return this.delegate.getIndexName(); + } + + public void setIndexName(String indexName) { + this.delegate.setIndexName(indexName); + } + + @DatabaseChangeProperty(description = "Tablepace to create the index in.") + public String getTablespace() { + return this.delegate.getTablespace(); + } + + public void setTablespace(String tablespace) { + this.delegate.setTablespace(tablespace); + } + + @DatabaseChangeProperty(description = "Unique values index", since = "1.8") + public Boolean isUnique() { + return this.delegate.isUnique(); + } + + public void setUnique(Boolean isUnique) { + this.delegate.setUnique(isUnique); + } + + @DatabaseChangeProperty(isChangeProperty = false) + public String getAssociatedWith() { + return delegate.getAssociatedWith(); + } + + public void setAssociatedWith(String associatedWith) { + this.delegate.setAssociatedWith(associatedWith); + } + + @DatabaseChangeProperty + public Boolean getClustered() { + return this.delegate.getClustered(); + } + + public void setClustered(Boolean clustered) { + this.delegate.setClustered(clustered); + } + + @Override + public void addColumn(JsonEnabledColumnConfig column) { + delegate.addColumn(column); + } + + @Override + @DatabaseChangeProperty(mustEqualExisting = "index.column", description = "Column(s) to add to the index", requiredForDatabase = "all") + public List getColumns() { + return this.delegate.getColumns().stream().map(JsonEnabledColumnConfig.class::cast).collect(Collectors.toList()); + } + + @Override + public void setColumns(List columns) { + columns.forEach(this.delegate::addColumn); + } + + @Override + public String getConfirmationMessage() { + return delegate.getConfirmationMessage(); + } + + @Override + public SqlStatement[] generateStatements(Database database) { + return new SqlStatement[]{new CreateJsonIndexStatement(this.getCatalogName(), this.getSchemaName(), this.getTableName(), + this.getIndexName(), this.isUnique(), this.getAssociatedWith(), this.getTablespace(), this.getClustered(), + this.getColumns().toArray(new JsonEnabledColumnConfig[0]))}; + } + + @Override + protected Change[] createInverses() { + DropIndexChange inverse = new DropIndexChange(); + inverse.setSchemaName(getSchemaName()); + inverse.setTableName(getTableName()); + inverse.setIndexName(getIndexName()); + return new Change[]{inverse}; + } + + @Override + public ChangeStatus checkStatus(Database database) { + return delegate.checkStatus(database); + } + + @Override + public String getSerializedObjectNamespace() { + return GENERIC_CHANGELOG_EXTENSION_NAMESPACE; + } + + @Override + public Object getSerializableFieldValue(String field) { + return delegate.getSerializableFieldValue(field); + } +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/extension/CreateJsonIndexGenerator.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/extension/CreateJsonIndexGenerator.java new file mode 100644 index 0000000000..021fade57d --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/extension/CreateJsonIndexGenerator.java @@ -0,0 +1,112 @@ +/* + * Copyright 2021 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.models.map.storage.jpa.liquibase.extension; + +import java.util.Arrays; +import java.util.stream.Collectors; + +import liquibase.database.Database; +import liquibase.database.core.PostgresDatabase; +import liquibase.exception.ValidationErrors; +import liquibase.sql.Sql; +import liquibase.sql.UnparsedSql; +import liquibase.sqlgenerator.SqlGenerator; +import liquibase.sqlgenerator.SqlGeneratorChain; +import liquibase.sqlgenerator.core.AbstractSqlGenerator; +import liquibase.statement.core.CreateIndexStatement; +import liquibase.structure.core.Index; +import liquibase.structure.core.Table; +import liquibase.util.StringUtils; + +/** + * A {@link SqlGenerator} implementation that supports {@link CreateJsonIndexStatement}s. It generates the SQL required + * to create an index for properties of JSON files stored in one of the table columns. + * + * @author Stefan Guilhen + */ +public class CreateJsonIndexGenerator extends AbstractSqlGenerator { + + /** + * Override the priority. This is needed because {@link CreateJsonIndexStatement} is a subtype of {@link CreateIndexStatement} + * and is thus a match for the standard index generators. By increasing the priority we ensure this is processed before + * the other generators. + * + * @return this generator's priority. + */ + @Override + public int getPriority() { + return SqlGenerator.PRIORITY_DATABASE + 1; + } + + @Override + public ValidationErrors validate(CreateJsonIndexStatement createIndexStatement, Database database, SqlGeneratorChain sqlGeneratorChain) { + ValidationErrors validationErrors = new ValidationErrors(); + validationErrors.checkRequiredField("tableName", createIndexStatement.getTableName()); + validationErrors.checkRequiredField("columns", createIndexStatement.getColumns()); + Arrays.stream(createIndexStatement.getColumns()).map(JsonEnabledColumnConfig.class::cast) + .forEach(config -> { + validationErrors.checkRequiredField("jsonColumn", config.getJsonColumn()); + validationErrors.checkRequiredField("jsonProperty", config.getJsonProperty()); + }); + return validationErrors; + } + + @Override + public Sql[] generateSql(CreateJsonIndexStatement statement, Database database, SqlGeneratorChain sqlGeneratorChain) { + + if (!(database instanceof PostgresDatabase)) { + // for now return an empty SQL for DBs that don't support JSON indexes natively. + return new Sql[0]; + } + + StringBuilder builder = new StringBuilder(); + builder.append("CREATE "); + if (statement.isUnique() != null && statement.isUnique()) { + builder.append("UNIQUE "); + } + builder.append("INDEX "); + + if (statement.getIndexName() != null) { + builder.append(database.escapeObjectName(statement.getIndexName(), Index.class)).append(" "); + } + + builder.append("ON ").append(database.escapeTableName(statement.getTableCatalogName(), statement.getTableSchemaName(), + statement.getTableName())); + this.handleJsonIndex(statement, database, builder); + if (StringUtils.trimToNull(statement.getTablespace()) != null && database.supportsTablespaces()) { + builder.append(" TABLESPACE ").append(statement.getTablespace()); + } + + return new Sql[]{new UnparsedSql(builder.toString(), getAffectedIndex(statement))}; + } + + protected void handleJsonIndex(final CreateJsonIndexStatement statement, final Database database, final StringBuilder builder) { + if (database instanceof PostgresDatabase) { + builder.append(" USING gin ("); + builder.append(Arrays.stream(statement.getColumns()).map(JsonEnabledColumnConfig.class::cast) + .map(c -> "(" + c.getJsonColumn() + "->'" + c.getJsonProperty() + "') jsonb_path_ops") + .collect(Collectors.joining(", "))) + .append(")"); + } + } + + protected Index getAffectedIndex(CreateIndexStatement statement) { + return new Index().setName(statement.getIndexName()).setTable((Table) new Table().setName(statement.getTableName()) + .setSchema(statement.getTableCatalogName(), statement.getTableSchemaName())); + } +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/extension/CreateJsonIndexStatement.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/extension/CreateJsonIndexStatement.java new file mode 100644 index 0000000000..311df28249 --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/extension/CreateJsonIndexStatement.java @@ -0,0 +1,39 @@ +/* + * Copyright 2021 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.models.map.storage.jpa.liquibase.extension; + +import liquibase.change.AddColumnConfig; +import liquibase.statement.core.CreateIndexStatement; + +/** + * A {@link liquibase.statement.SqlStatement} that holds the information needed to create JSON indexes. Having a specific + * subtype allows for easier selection of the respective {@link liquibase.sqlgenerator.SqlGenerator}, since Liquibase + * selects the generators based on the statement type they are capable of handling. + * + * @author Stefan Guilhen + */ +public class CreateJsonIndexStatement extends CreateIndexStatement { + + public CreateJsonIndexStatement(final String tableCatalogName, final String tableSchemaName, final String tableName, + final String indexName, final Boolean isUnique, final String associatedWith, + final String tablespace, final Boolean clustered, final AddColumnConfig... columns) { + super(indexName, tableCatalogName, tableSchemaName, tableName, isUnique, associatedWith, columns); + super.setTablespace(tablespace); + super.setClustered(clustered); + } +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/extension/GeneratedColumnChange.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/extension/GeneratedColumnChange.java new file mode 100644 index 0000000000..048adca783 --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/extension/GeneratedColumnChange.java @@ -0,0 +1,216 @@ +/* + * Copyright 2021 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.models.map.storage.jpa.liquibase.extension; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import liquibase.change.AbstractChange; +import liquibase.change.AddColumnConfig; +import liquibase.change.Change; +import liquibase.change.ChangeMetaData; +import liquibase.change.ChangeStatus; +import liquibase.change.ChangeWithColumns; +import liquibase.change.ColumnConfig; +import liquibase.change.DatabaseChange; +import liquibase.change.DatabaseChangeProperty; +import liquibase.change.core.AddColumnChange; +import liquibase.database.Database; +import liquibase.database.core.PostgresDatabase; +import liquibase.exception.ValidationErrors; +import liquibase.statement.SqlStatement; +import liquibase.statement.core.AddColumnStatement; + +/** + * Extension used to a column whose values are generated from a property of a JSON file stored in one of the table's columns. + *

+ * Example configuration in the changelog: + *

+ *     <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ *                    xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
+ *                    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
+ *                    http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd
+ *                    http://www.liquibase.org/xml/ns/dbchangelog-ext
+ *                    http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
+ *
+ *     <changeSet author="keycloak" id="some_id">
+ *         ...
+ *         <ext:addGeneratedColumn tableName="test">
+ *             <ext:column name="new_column" type="VARCHAR(36)" jsonColumn="metadata" jsonProperty="alias"/>
+ *         </ext:addGeneratedColumn>
+ *     </changeSet>
+ * 
+ * The above configuration is adding a new column, named {@code new_column}, whose values are generated from the {@code alias} property + * of the JSON file stored in column {@code metadata}. If, for example, a particular entry in the table contains the JSON + * {@code {"name":"duke","alias":"jduke"}} in column {@code metadata}, the value generated for the new column will be {@code jduke}. + * + * @author Stefan Guilhen + */ +@DatabaseChange(name = "addGeneratedColumn", description = "Adds new generated columns to a table. The columns must reference a JSON property inside an existing JSON column.", + priority = ChangeMetaData.PRIORITY_DEFAULT, appliesTo = "table") +public class GeneratedColumnChange extends AbstractChange implements ChangeWithColumns { + + private final ExtendedAddColumnChange delegate; + private Map configMap = new HashMap<>(); + + public GeneratedColumnChange() { + this.delegate = new ExtendedAddColumnChange(); + } + + @DatabaseChangeProperty(mustEqualExisting ="relation.catalog") + public String getCatalogName() { + return this.delegate.getCatalogName(); + } + + public void setCatalogName(final String catalogName) { + this.delegate.setCatalogName(catalogName); + } + + @DatabaseChangeProperty(mustEqualExisting ="relation.schema") + public String getSchemaName() { + return this.delegate.getSchemaName(); + } + + public void setSchemaName(final String schemaName) { + this.delegate.setSchemaName(schemaName); + } + + @DatabaseChangeProperty(mustEqualExisting ="table", description = "Name of the table to add the generated column to") + public String getTableName() { + return this.delegate.getTableName(); + } + + public void setTableName(final String tableName) { + this.delegate.setTableName(tableName); + } + + @Override + public void addColumn(final JsonEnabledColumnConfig column) { + this.delegate.addColumn(column); + this.configMap.put(column.getName(), column); + } + + @Override + @DatabaseChangeProperty(description = "Generated columns information", requiredForDatabase = "all") + public List getColumns() { + return this.delegate.getColumns().stream().map(JsonEnabledColumnConfig.class::cast).collect(Collectors.toList()); + } + + @Override + public void setColumns(final List columns) { + columns.forEach(this.delegate::addColumn); + this.configMap = this.getColumns().stream() + .collect(Collectors.toMap(ColumnConfig::getName, Function.identity())); + } + + @Override + public SqlStatement[] generateStatements(Database database) { + if (database instanceof PostgresDatabase) { + for (AddColumnConfig config : delegate.getColumns()) { + String columnType = config.getType(); + // if postgres, change JSON type to JSONB before generating the statements as JSONB is more efficient. + if (columnType.equalsIgnoreCase("JSON")) { + config.setType("JSONB"); + } + } + } + + // AddColumnChange always produces an AddColumnStatement in the first position of the returned array. + AddColumnStatement delegateStatement = (AddColumnStatement) Arrays.stream(this.delegate.generateStatements(database)) + .findFirst().get(); + + // convert the regular AddColumnStatements into GeneratedColumnStatements, adding the extension properties. + if (!delegateStatement.isMultiple()) { + // single statement - convert it directly. + JsonEnabledColumnConfig config = configMap.get(delegateStatement.getColumnName()); + if (config != null) { + return new SqlStatement[] {new GeneratedColumnStatement(delegateStatement, config.getJsonColumn(), + config.getJsonProperty())}; + } + } + else { + // multiple statement - convert all sub-statements. + List generatedColumnStatements = delegateStatement.getColumns().stream() + .filter(c -> configMap.containsKey(c.getColumnName())) + .map(c -> new GeneratedColumnStatement(c, configMap.get(c.getColumnName()).getJsonColumn(), + configMap.get(c.getColumnName()).getJsonProperty())) + .collect(Collectors.toList()); + + // add all GeneratedColumnStatements into a composite statement and return the composite. + return new SqlStatement[]{new GeneratedColumnStatement(generatedColumnStatements)}; + } + return new SqlStatement[0]; + } + + @Override + protected Change[] createInverses() { + return this.delegate.createInverses(); + } + + @Override + public ChangeStatus checkStatus(Database database) { + return delegate.checkStatus(database); + } + + @Override + public String getConfirmationMessage() { + return delegate.getConfirmationMessage(); + } + + @Override + public String getSerializedObjectNamespace() { + return GENERIC_CHANGELOG_EXTENSION_NAMESPACE; + } + + @Override + public ValidationErrors validate(Database database) { + ValidationErrors validationErrors = new ValidationErrors(); + validationErrors.checkRequiredField("columns", this.delegate.getColumns()); + // validate each generated column. + this.delegate.getColumns().stream().map(JsonEnabledColumnConfig.class::cast).forEach( + config -> { + if (config.isAutoIncrement() != null && config.isAutoIncrement()) { + validationErrors.addError("Generated column " + config.getName() + " cannot be auto-incremented"); + } else if (config.getValueObject() != null) { + validationErrors.addError("Generated column " + config.getName() + " cannot be configured with a value"); + } else if (config.getDefaultValueObject() != null) { + validationErrors.addError("Generated column " + config.getName() + " cannot be configured with a default value"); + } + // we can expand this check if we decide to allow other types of generated columns in the future - for now + // ensure the column can be properly generated from a json property stored on a json column. + validationErrors.checkRequiredField("jsonColumn", config.getJsonColumn()); + validationErrors.checkRequiredField("jsonProperty", config.getJsonProperty()); + }); + validationErrors.addAll(super.validate(database)); + return validationErrors; + } + + /** + * Simple extension that makes protected methods public so they can be accessed as a delegate. + */ + private static class ExtendedAddColumnChange extends AddColumnChange { + @Override + public Change[] createInverses() { + return super.createInverses(); + } + } +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/extension/GeneratedColumnSqlGenerator.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/extension/GeneratedColumnSqlGenerator.java new file mode 100644 index 0000000000..b953db726b --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/extension/GeneratedColumnSqlGenerator.java @@ -0,0 +1,96 @@ +/* + * Copyright 2021 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.models.map.storage.jpa.liquibase.extension; + +import java.util.ArrayList; +import java.util.List; + +import liquibase.database.Database; +import liquibase.database.core.PostgresDatabase; +import liquibase.sql.Sql; +import liquibase.sql.UnparsedSql; +import liquibase.sqlgenerator.SqlGenerator; +import liquibase.sqlgenerator.core.AddColumnGenerator; +import liquibase.statement.core.AddColumnStatement; + +/** + * A {@link SqlGenerator} implementation that supports {@link GeneratedColumnStatement}s. It generates the SQL required + * to add a column whose values are generated from a property of a JSON file stored in one of the table columns. + * + * @author Stefan Guilhen + */ +public class GeneratedColumnSqlGenerator extends AddColumnGenerator { + + /** + * Override the priority. This is needed because {@link GeneratedColumnStatement} is a subtype of {@link AddColumnStatement} + * and is thus a match for the standard column generators. By increasing the priority we ensure this is processed before + * the other generators. + * + * @return this generator's priority. + */ + @Override + public int getPriority() { + return SqlGenerator.PRIORITY_DEFAULT + 1; + } + + /** + * Implement {@link #supports(AddColumnStatement, Database)} to return {@code true} only if the statement type is an instance + * of {@link GeneratedColumnStatement}. + *

+ * This is needed because this generator is a sub-class of {@link AddColumnGenerator} and is thus registered as being + * able to handle statements of type {@link AddColumnStatement}. Due to the increased priority, this generator ends up + * being selected to handle standard {@code addColumn} changes, which is not desirable. By returning {@code true} only + * when the statement is a {@link GeneratedColumnStatement} we ensure this implementation is selected only when a generated + * column is being added, allowing liquibase to continue iterating through the chain of generators in order to select the + * right generator to handle the standard {@code addColumn} changes. + * + * @param statement the {@link liquibase.statement.SqlStatement} to be processed. + * @param database a reference to the database. + * @return {@code true} if an only if the statement is a {@link GeneratedColumnStatement}; {@code false} otherwise. + */ + @Override + public boolean supports(AddColumnStatement statement, Database database) { + // use this implementation for generated columns only. + return statement instanceof GeneratedColumnStatement; + } + + @Override + protected Sql[] generateSingleColumn(final AddColumnStatement statement, final Database database) { + + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append(super.generateSingleColumBaseSQL(statement, database)); + sqlBuilder.append(super.generateSingleColumnSQL(statement, database)); + this.handleGeneratedColumn((GeneratedColumnStatement) statement, database, sqlBuilder); + + List returnSql = new ArrayList<>(); + returnSql.add(new UnparsedSql(sqlBuilder.toString(), super.getAffectedColumn(statement))); + + super.addUniqueConstrantStatements(statement, database, returnSql); + super.addForeignKeyStatements(statement, database, returnSql); + + return returnSql.toArray(new Sql[0]); + } + + protected void handleGeneratedColumn(final GeneratedColumnStatement statement, final Database database, final StringBuilder sqlBuilder) { + if (database instanceof PostgresDatabase) { + // assemble the GENERATED ALWAYS AS section of the query using the json property selection function. + sqlBuilder.append(" GENERATED ALWAYS AS ((").append(statement.getJsonColumn()).append("->>'").append(statement.getJsonProperty()) + .append("')::").append(statement.getColumnType()).append(") stored"); + } + } +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/extension/GeneratedColumnStatement.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/extension/GeneratedColumnStatement.java new file mode 100644 index 0000000000..3a645b2185 --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/extension/GeneratedColumnStatement.java @@ -0,0 +1,65 @@ +/* + * Copyright 2021 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.models.map.storage.jpa.liquibase.extension; + +import java.util.List; + +import liquibase.statement.ColumnConstraint; +import liquibase.statement.core.AddColumnStatement; + +/** + * A {@link liquibase.statement.SqlStatement} that extends the standard {@link AddColumnStatement} to include properties + * to identify the JSON column and JSON property that are to be used to generated the values for the column being added. + * + * @author Stefan Guilhen + */ +public class GeneratedColumnStatement extends AddColumnStatement { + + private String jsonColumn; + private String jsonProperty; + + public GeneratedColumnStatement(final AddColumnStatement statement, final String jsonColumn, final String jsonProperty) { + super(statement.getCatalogName(), statement.getSchemaName(), statement.getTableName(), statement.getColumnName(), + statement.getColumnType(), statement.getDefaultValue(), statement.getRemarks(), + statement.getConstraints().toArray(new ColumnConstraint[0])); + this.jsonColumn = jsonColumn; + this.jsonProperty = jsonProperty; + } + + public GeneratedColumnStatement(final List statements) { + super(statements.toArray(new GeneratedColumnStatement[0])); + } + + /** + * Obtains the name of the column that holds JSON files. + * + * @return the name of the JSON column. + */ + public String getJsonColumn() { + return this.jsonColumn; + } + + /** + * Obtains the name of the property in the JSON file whose value is to be used as the generated value for the new column. + * + * @return the name of the JSON property. + */ + public String getJsonProperty() { + return this.jsonProperty; + } +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/extension/JsonDataType.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/extension/JsonDataType.java new file mode 100644 index 0000000000..8e6a10796f --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/extension/JsonDataType.java @@ -0,0 +1,42 @@ +/* + * Copyright 2021 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.models.map.storage.jpa.liquibase.extension; + +import liquibase.database.Database; +import liquibase.database.core.PostgresDatabase; +import liquibase.datatype.DataTypeInfo; +import liquibase.datatype.DatabaseDataType; +import liquibase.datatype.LiquibaseDataType; + +/** + * A {@link LiquibaseDataType} to handle the JSON column type. + * + * @author Stefan Guilhen + */ +@DataTypeInfo(name="json", minParameters = 0, maxParameters = 1, priority = LiquibaseDataType.PRIORITY_DEFAULT + 1) +public class JsonDataType extends LiquibaseDataType { + + @Override + public DatabaseDataType toDatabaseDataType(Database database) { + if (database instanceof PostgresDatabase) { + // on Postgres switch the columns of type JSON to JSONB as JSONB is a more efficient type to handle JSON contents. + return new DatabaseDataType("JSONB", super.getParameters()); + } + return super.toDatabaseDataType(database); + } +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/extension/JsonEnabledColumnConfig.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/extension/JsonEnabledColumnConfig.java new file mode 100644 index 0000000000..2b261ba910 --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/liquibase/extension/JsonEnabledColumnConfig.java @@ -0,0 +1,81 @@ +/* + * Copyright 2021 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.models.map.storage.jpa.liquibase.extension; + +import liquibase.change.AddColumnConfig; +import liquibase.parser.core.ParsedNode; +import liquibase.parser.core.ParsedNodeException; +import liquibase.resource.ResourceAccessor; + +/** + * A {@link liquibase.change.ColumnConfig} extension that contains attributes to specify a JSON column and the property + * to be selected from the JSON file. + *

+ * This config is used by extensions that need to operated on data stored in JSON columns. + * + * @author Stefan Guilhen + */ +public class JsonEnabledColumnConfig extends AddColumnConfig { + + private String jsonColumn; + private String jsonProperty; + + /** + * Obtains the name of the column that contains JSON files. + * + * @return the JSON column name. + */ + public String getJsonColumn() { + return this.jsonColumn; + } + + /** + * Sets the name of the column that contains JSON files. + * + * @param jsonColumn the name of the JSON column. + */ + public void setJsonColumn(final String jsonColumn) { + this.jsonColumn = jsonColumn; + } + + /** + * Obtains the name of the property inside the JSON file. + * + * @return the name of the JSON property. + */ + public String getJsonProperty() { + return this.jsonProperty; + } + + /** + * Sets the name of the property inside the JSON file. + * + * @param jsonProperty the name of the JSON property. + */ + public void setJsonProperty(final String jsonProperty) { + this.jsonProperty = jsonProperty; + } + + @Override + public void load(ParsedNode parsedNode, ResourceAccessor resourceAccessor) throws ParsedNodeException { + // load the standard column attributs and then load the JSON attributes. + super.load(parsedNode, resourceAccessor); + this.jsonColumn = parsedNode.getChildValue(null, "jsonColumn", String.class); + this.jsonProperty = parsedNode.getChildValue(null, "jsonProperty", String.class); + } +} diff --git a/model/map-jpa/src/main/resources/META-INF/jpa-clients-changelog-1.xml b/model/map-jpa/src/main/resources/META-INF/jpa-clients-changelog-1.xml index bcdd0299e8..fe1271093d 100644 --- a/model/map-jpa/src/main/resources/META-INF/jpa-clients-changelog-1.xml +++ b/model/map-jpa/src/main/resources/META-INF/jpa-clients-changelog-1.xml @@ -17,37 +17,58 @@ limitations under the License. --> - + - - create table client ( - id uuid primary key not null, - entityVersion integer generated always as ((metadata->>'entityVersion')::int) stored, - realmId varchar(36) generated always as (metadata->>'fRealmId') stored, - clientId varchar(255) generated always as (metadata->>'fClientId') stored, - protocol varchar(36) generated always as (metadata->>'fProtocol') stored, - enabled boolean generated always as ((metadata->>'fEnabled')::boolean) stored, - metadata jsonb - ); - - create index client_entityVersion on client(entityVersion); - create index client_realmId_clientId on client(realmId, clientId); - create index client_scopeMappings on client using gin ((metadata->'fScopeMappings') jsonb_path_ops); - - create table client_attribute ( - id uuid primary key not null, - fk_client uuid references client(id) on delete cascade, - name varchar(255), - value text - ); - - create index client_attr_fk_client on client_attribute(fk_client); - create index client_attr_name_value on client_attribute(name, (value::varchar(250))); - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/model/map-jpa/src/main/resources/META-INF/services/org.keycloak.models.map.storage.jpa.liquibase.MapLiquibaseConnectionProviderFactory b/model/map-jpa/src/main/resources/META-INF/services/org.keycloak.models.map.storage.jpa.liquibase.MapLiquibaseConnectionProviderFactory new file mode 100644 index 0000000000..b2a8631e9f --- /dev/null +++ b/model/map-jpa/src/main/resources/META-INF/services/org.keycloak.models.map.storage.jpa.liquibase.MapLiquibaseConnectionProviderFactory @@ -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.models.map.storage.jpa.liquibase.DefaultLiquibaseConnectionProviderFactory \ No newline at end of file diff --git a/model/map-jpa/src/main/resources/META-INF/services/org.keycloak.models.map.storage.jpa.updater.MapJpaUpdaterProviderFactory b/model/map-jpa/src/main/resources/META-INF/services/org.keycloak.models.map.storage.jpa.updater.MapJpaUpdaterProviderFactory index 84cb2376bd..d6d2dfb6d6 100644 --- a/model/map-jpa/src/main/resources/META-INF/services/org.keycloak.models.map.storage.jpa.updater.MapJpaUpdaterProviderFactory +++ b/model/map-jpa/src/main/resources/META-INF/services/org.keycloak.models.map.storage.jpa.updater.MapJpaUpdaterProviderFactory @@ -15,4 +15,4 @@ # limitations under the License. # -org.keycloak.models.map.storage.jpa.updater.liquibase.MapJpaLiquibaseUpdaterProviderFactory +org.keycloak.models.map.storage.jpa.liquibase.MapJpaLiquibaseUpdaterProviderFactory diff --git a/model/map-jpa/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/model/map-jpa/src/main/resources/META-INF/services/org.keycloak.provider.Spi index a6ae8657b7..9c0a2fe0dd 100644 --- a/model/map-jpa/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/model/map-jpa/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -16,3 +16,4 @@ # org.keycloak.models.map.storage.jpa.updater.MapJpaUpdaterSpi +org.keycloak.models.map.storage.jpa.liquibase.MapLiquibaseConnectionSpi