aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-db-migration
diff options
context:
space:
mode:
authorJacek <jacek.poreda@sonarsource.com>2021-08-12 16:55:04 +0200
committersonartech <sonartech@sonarsource.com>2021-09-08 20:03:34 +0000
commitcfe5faf0b14841bdad0cae24ac89832c98d1d32a (patch)
treeb089d578bc9b598f46c79e0c22d254fafafcc1fe /server/sonar-db-migration
parent6ad72a24b224d7949ad790b70ef4ae8352ba1899 (diff)
downloadsonarqube-cfe5faf0b14841bdad0cae24ac89832c98d1d32a.tar.gz
sonarqube-cfe5faf0b14841bdad0cae24ac89832c98d1d32a.zip
SONAR-15259 Migrate portfolios from XML to DB
Diffstat (limited to 'server/sonar-db-migration')
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v91/CreatePortfoliosTable.java2
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v91/DbVersion91.java1
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v91/MigratePortfoliosToNewTables.java581
-rw-r--r--server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v91/DbVersion91Test.java2
-rw-r--r--server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v91/MigratePortfoliosToNewTablesTest.java431
-rw-r--r--server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v91/MigratePortfoliosToNewTablesTest/schema.sql110
6 files changed, 1125 insertions, 2 deletions
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v91/CreatePortfoliosTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v91/CreatePortfoliosTable.java
index a25d3b1adcf..26d9035fcbc 100644
--- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v91/CreatePortfoliosTable.java
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v91/CreatePortfoliosTable.java
@@ -40,7 +40,7 @@ public class CreatePortfoliosTable extends CreateTableChange {
public void execute(Context context, String tableName) throws SQLException {
context.execute(new CreateTableBuilder(getDialect(), tableName)
.addPkColumn(newVarcharColumnDefBuilder().setColumnName("uuid").setIsNullable(false).setLimit(UUID_SIZE).build())
- .addColumn(newVarcharColumnDefBuilder().setColumnName("kee").setIsNullable(false).setLimit(UUID_SIZE).build())
+ .addColumn(newVarcharColumnDefBuilder().setColumnName("kee").setIsNullable(false).setLimit(400).build())
.addColumn(newVarcharColumnDefBuilder().setColumnName("name").setIsNullable(false).setLimit(2000).build())
.addColumn(newVarcharColumnDefBuilder().setColumnName("description").setIsNullable(true).setLimit(2000).build())
.addColumn(newVarcharColumnDefBuilder().setColumnName("root_uuid").setIsNullable(false).setLimit(UUID_SIZE).build())
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v91/DbVersion91.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v91/DbVersion91.java
index 45dc9309a74..6a000293a25 100644
--- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v91/DbVersion91.java
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v91/DbVersion91.java
@@ -39,6 +39,7 @@ public class DbVersion91 implements DbVersion {
.add(6011, "Create 'portfolios' table", CreatePortfoliosTable.class)
.add(6012, "Create 'portfolio_references' table", CreatePortfolioReferencesTable.class)
.add(6013, "Create 'portfolio_projects' table", CreatePortfolioProjectsTable.class)
+ .add(6014, "Migrate portfolios to new tables", MigratePortfoliosToNewTables.class)
;
}
}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v91/MigratePortfoliosToNewTables.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v91/MigratePortfoliosToNewTables.java
new file mode 100644
index 00000000000..377f7261c28
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v91/MigratePortfoliosToNewTables.java
@@ -0,0 +1,581 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.version.v91;
+
+import com.google.common.collect.Sets;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.StringReader;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import javax.xml.XMLConstants;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.transform.sax.SAXSource;
+import javax.xml.validation.SchemaFactory;
+import org.apache.commons.lang.StringUtils;
+import org.codehaus.staxmate.SMInputFactory;
+import org.codehaus.staxmate.in.SMHierarchicCursor;
+import org.codehaus.staxmate.in.SMInputCursor;
+import org.sonar.api.utils.System2;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.Database;
+import org.sonar.db.DatabaseUtils;
+import org.sonar.server.platform.db.migration.step.DataChange;
+import org.sonar.server.platform.db.migration.step.MassUpdate;
+import org.sonar.server.platform.db.migration.step.Select;
+import org.sonar.server.platform.db.migration.step.Upsert;
+import org.sonar.server.platform.db.migration.version.v91.MigratePortfoliosToNewTables.ViewXml.ViewDef;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Optional.ofNullable;
+import static javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING;
+import static org.apache.commons.io.IOUtils.toInputStream;
+import static org.apache.commons.lang.StringUtils.trim;
+
+public class MigratePortfoliosToNewTables extends DataChange {
+ private static final String SELECT_DEFAULT_VISIBILITY = "select text_value from properties where prop_key = 'projects.default.visibility'";
+ private static final String SELECT_UUID_VISIBILITY_BY_COMPONENT_KEY = "select c.uuid, c.private from components c where c.kee = ?";
+ private static final String SELECT_PORTFOLIO_UUID_AND_SELECTION_MODE_BY_KEY = "select uuid,selection_mode from portfolios where kee = ?";
+ private static final String SELECT_PROJECT_KEYS_BY_PORTFOLIO_UUID = "select p.kee from portfolio_projects pp "
+ + "join projects p on pp.project_uuid = p.uuid where pp.portfolio_uuid = ?";
+ private static final String SELECT_PROJECT_UUIDS_BY_KEYS = "select p.uuid,p.kee from projects p where p.kee in (PLACEHOLDER)";
+ private static final String VIEWS_DEF_KEY = "views.def";
+ private static final String PLACEHOLDER = "PLACEHOLDER";
+
+ private final UuidFactory uuidFactory;
+ private final System2 system;
+
+ private boolean defaultPrivateFlag;
+
+ public enum SelectionMode {
+ NONE, MANUAL, REGEXP, REST, TAGS
+ }
+
+ public MigratePortfoliosToNewTables(Database db, UuidFactory uuidFactory, System2 system) {
+ super(db);
+
+ this.uuidFactory = uuidFactory;
+ this.system = system;
+ }
+
+ @Override
+ protected void execute(Context context) throws SQLException {
+ String xml = getViewsDefinition(context);
+ // skip migration if `views.def` does not exist in the db
+ if (xml == null) {
+ return;
+ }
+
+ this.defaultPrivateFlag = ofNullable(context.prepareSelect(SELECT_DEFAULT_VISIBILITY)
+ .get(row -> "private".equals(row.getString(1))))
+ .orElse(false);
+
+ try {
+ Map<String, ViewXml.ViewDef> portfolioXmlMap = ViewXml.parse(xml);
+ List<ViewXml.ViewDef> portfolios = new LinkedList<>(portfolioXmlMap.values());
+
+ Map<String, PortfolioDb> portfolioDbMap = new HashMap<>();
+ for (ViewXml.ViewDef portfolio : portfolios) {
+ PortfolioDb createdPortfolio = insertPortfolio(context, portfolio);
+ if (createdPortfolio.selectionMode == SelectionMode.MANUAL) {
+ insertPortfolioProjects(context, portfolio, createdPortfolio);
+ }
+ portfolioDbMap.put(createdPortfolio.kee, createdPortfolio);
+ }
+ // all portfolio has been created and new uuids assigned
+ // update portfolio hierarchy parent/root
+ insertReferences(context, portfolioXmlMap, portfolioDbMap);
+ updateHierarchy(context, portfolioXmlMap, portfolioDbMap);
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to migrate views definitions property.", e);
+ }
+ }
+
+ private PortfolioDb insertPortfolio(Context context, ViewXml.ViewDef portfolioFromXml) throws SQLException {
+ long now = system.now();
+ PortfolioDb portfolioDb = context.prepareSelect(SELECT_PORTFOLIO_UUID_AND_SELECTION_MODE_BY_KEY)
+ .setString(1, portfolioFromXml.key)
+ .get(r -> new PortfolioDb(r.getString(1), portfolioFromXml.key, SelectionMode.valueOf(r.getString(2))));
+
+ Optional<ComponentDb> componentDbOpt = ofNullable(context.prepareSelect(SELECT_UUID_VISIBILITY_BY_COMPONENT_KEY)
+ .setString(1, portfolioFromXml.key)
+ .get(row -> new ComponentDb(row.getString(1), row.getBoolean(2))));
+
+ // no portfolio -> insert
+ if (portfolioDb == null) {
+ Upsert insertPortfolioQuery = context.prepareUpsert("insert into " +
+ "portfolios(uuid, kee, private, name, description, root_uuid, parent_uuid, selection_mode, selection_expression, updated_at, created_at) " +
+ "values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
+
+ String portfolioUuid = componentDbOpt.map(c -> c.uuid).orElse(uuidFactory.create());
+ insertPortfolioQuery.setString(1, portfolioUuid)
+ .setString(2, portfolioFromXml.key)
+ .setBoolean(3, componentDbOpt.map(c -> c.visibility).orElse(this.defaultPrivateFlag))
+ .setString(4, portfolioFromXml.name)
+ .setString(5, portfolioFromXml.desc)
+ .setString(6, PLACEHOLDER)
+ .setString(7, PLACEHOLDER);
+ SelectionMode selectionMode = SelectionMode.NONE;
+ String selectionExpression = null;
+ if (portfolioFromXml.getProjects() != null && !portfolioFromXml.getProjects().isEmpty()) {
+ selectionMode = SelectionMode.MANUAL;
+ } else if (portfolioFromXml.regexp != null && !portfolioFromXml.regexp.isBlank()) {
+ selectionMode = SelectionMode.REGEXP;
+ selectionExpression = portfolioFromXml.regexp;
+ } else if (portfolioFromXml.def) {
+ selectionMode = SelectionMode.REST;
+ } else if (portfolioFromXml.tagsAssociation != null && !portfolioFromXml.tagsAssociation.isEmpty()) {
+ selectionMode = SelectionMode.TAGS;
+ selectionExpression = String.join(",", portfolioFromXml.tagsAssociation);
+ }
+
+ insertPortfolioQuery.setString(8, selectionMode.name())
+ .setString(9, selectionExpression)
+ // set dates
+ .setLong(10, now)
+ .setLong(11, now)
+ .execute()
+ .commit();
+ return new PortfolioDb(portfolioUuid, portfolioFromXml.key, selectionMode);
+ }
+ return portfolioDb;
+ }
+
+ private void insertPortfolioProjects(Context context, ViewDef portfolio, PortfolioDb createdPortfolio) throws SQLException {
+ long now = system.now();
+ // select all already added project uuids
+ Set<String> alreadyAddedPortfolioProjects = new HashSet<>(
+ context.prepareSelect(SELECT_PROJECT_KEYS_BY_PORTFOLIO_UUID)
+ .setString(1, createdPortfolio.uuid)
+ .list(r -> r.getString(1)));
+
+ Set<String> projectKeysFromXml = new HashSet<>(portfolio.getProjects());
+ Set<String> projectKeysToBeAdded = Sets.difference(projectKeysFromXml, alreadyAddedPortfolioProjects);
+
+ if (!projectKeysToBeAdded.isEmpty()) {
+ List<ProjectDb> projects = findPortfolioProjects(context, projectKeysToBeAdded);
+
+ var upsert = context.prepareUpsert("insert into " +
+ "portfolio_projects(uuid, portfolio_uuid, project_uuid, created_at) " +
+ "values (?, ?, ?, ?)");
+ for (ProjectDb projectDb : projects) {
+ upsert.setString(1, uuidFactory.create())
+ .setString(2, createdPortfolio.uuid)
+ .setString(3, projectDb.uuid)
+ .setLong(4, now)
+ .addBatch();
+ }
+ if (!projects.isEmpty()) {
+ upsert.execute()
+ .commit();
+ }
+ }
+ }
+
+ private static List<ProjectDb> findPortfolioProjects(Context context, Set<String> projectKeysToBeAdded) {
+ return DatabaseUtils.executeLargeInputs(projectKeysToBeAdded, keys -> {
+ try {
+ String selectQuery = SELECT_PROJECT_UUIDS_BY_KEYS.replace(PLACEHOLDER,
+ keys.stream().map(key -> "'" + key + "'").collect(
+ Collectors.joining(",")));
+ return context.prepareSelect(selectQuery)
+ .list(r -> new ProjectDb(r.getString(1), r.getString(2)));
+ } catch (SQLException e) {
+ throw new IllegalStateException("Could not execute 'in' query", e);
+ }
+ });
+ }
+
+ private void insertReferences(Context context, Map<String, ViewDef> portfolioXmlMap,
+ Map<String, PortfolioDb> portfolioDbMap) throws SQLException {
+ Upsert insertQuery = context.prepareUpsert("insert into portfolio_references(uuid, portfolio_uuid, reference_uuid, created_at) values (?, ?, ?, ?)");
+
+ long now = system.now();
+ boolean shouldExecuteQuery = false;
+ for (ViewDef portfolio : portfolioXmlMap.values()) {
+ var currentPortfolioUuid = portfolioDbMap.get(portfolio.key).uuid;
+ Set<String> referencesFromXml = new HashSet<>(portfolio.getReferences());
+ Set<String> referencesFromDb = new HashSet<>(context.prepareSelect("select pr.reference_uuid from portfolio_references pr where pr.portfolio_uuid = ?")
+ .setString(1, currentPortfolioUuid)
+ .list(row -> row.getString(1)));
+
+ for (String appOrPortfolio : referencesFromXml) {
+ // if portfolio and hasn't been added already
+ if (portfolioDbMap.containsKey(appOrPortfolio) && !referencesFromDb.contains(portfolioDbMap.get(appOrPortfolio).uuid)) {
+ insertQuery
+ .setString(1, uuidFactory.create())
+ .setString(2, currentPortfolioUuid)
+ .setString(3, portfolioDbMap.get(appOrPortfolio).uuid)
+ .setLong(4, now)
+ .addBatch();
+ shouldExecuteQuery = true;
+ } else {
+ // if application exist and haven't been added
+ String appUuid = context.prepareSelect("select p.uuid from projects p where p.kee = ?")
+ .setString(1, appOrPortfolio)
+ .get(row -> row.getString(1));
+ if (appUuid != null && !referencesFromDb.contains(appUuid)) {
+ insertQuery
+ .setString(1, uuidFactory.create())
+ .setString(2, currentPortfolioUuid)
+ .setString(3, appUuid)
+ .setLong(4, now)
+ .addBatch();
+ shouldExecuteQuery = true;
+ }
+ }
+ }
+ }
+ if (shouldExecuteQuery) {
+ insertQuery
+ .execute()
+ .commit();
+ }
+
+ }
+
+ private static void updateHierarchy(Context context, Map<String, ViewXml.ViewDef> defs, Map<String, PortfolioDb> portfolioDbMap) throws SQLException {
+ MassUpdate massUpdate = context.prepareMassUpdate();
+ massUpdate.select("select uuid, kee from portfolios where root_uuid = ? or parent_uuid = ?")
+ .setString(1, PLACEHOLDER)
+ .setString(2, PLACEHOLDER);
+ massUpdate.update("update portfolios set root_uuid = ?, parent_uuid = ? where uuid = ?");
+ massUpdate.execute((row, update) -> {
+ String currentPortfolioUuid = row.getString(1);
+ String currentPortfolioKey = row.getString(2);
+
+ var currentPortfolio = defs.get(currentPortfolioKey);
+ String parentUuid = ofNullable(currentPortfolio.parent).map(parent -> portfolioDbMap.get(parent).uuid).orElse(null);
+ String rootUuid = ofNullable(currentPortfolio.root).map(root -> portfolioDbMap.get(root).uuid).orElse(currentPortfolioUuid);
+ update.setString(1, rootUuid)
+ .setString(2, parentUuid)
+ .setString(3, currentPortfolioUuid);
+ return true;
+ });
+ }
+
+ @CheckForNull
+ private static String getViewsDefinition(DataChange.Context context) throws SQLException {
+ Select select = context.prepareSelect("select text_value,clob_value from internal_properties where kee=?");
+ select.setString(1, VIEWS_DEF_KEY);
+ return select.get(row -> {
+ String v = row.getString(1);
+ if (v != null) {
+ return v;
+ } else {
+ return row.getString(2);
+ }
+ });
+ }
+
+ private static class ComponentDb {
+ String uuid;
+ boolean visibility;
+
+ public ComponentDb(String uuid, boolean visibility) {
+ this.uuid = uuid;
+ this.visibility = visibility;
+ }
+ }
+
+ private static class PortfolioDb {
+ String uuid;
+ String kee;
+ SelectionMode selectionMode;
+
+ PortfolioDb(String uuid, String kee,
+ SelectionMode selectionMode) {
+ this.uuid = uuid;
+ this.kee = kee;
+ this.selectionMode = selectionMode;
+ }
+ }
+
+ private static class ProjectDb {
+ String uuid;
+ String kee;
+
+ ProjectDb(String uuid, String kee) {
+ this.uuid = uuid;
+ this.kee = kee;
+ }
+ }
+
+ static class ViewXml {
+ static final String SCHEMA_VIEWS = "/static/views.xsd";
+ static final String VIEWS_HEADER_BARE = "<views>";
+ static final Pattern VIEWS_HEADER_BARE_PATTERN = Pattern.compile(VIEWS_HEADER_BARE);
+ static final String VIEWS_HEADER_FQ = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+ + "<views xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://sonarsource.com/schema/views\">";
+
+ private ViewXml() {
+ // nothing to do here
+ }
+
+ private static Map<String, ViewDef> parse(String xml) throws ParserConfigurationException, SAXException, IOException, XMLStreamException {
+ if (StringUtils.isEmpty(xml)) {
+ return new LinkedHashMap<>(0);
+ }
+
+ List<ViewDef> views;
+ validate(xml);
+ SMInputFactory inputFactory = initStax();
+ SMHierarchicCursor rootC = inputFactory.rootElementCursor(new StringReader(xml));
+ rootC.advance(); // <views>
+ SMInputCursor cursor = rootC.childElementCursor();
+ views = parseViewDefinitions(cursor);
+
+ Map<String, ViewDef> result = new LinkedHashMap<>(views.size());
+ for (ViewDef def : views) {
+ result.put(def.key, def);
+ }
+
+ return result;
+ }
+
+ private static void validate(String xml) throws IOException, SAXException, ParserConfigurationException {
+ // Replace bare, namespace unaware header with fully qualified header (with schema declaration)
+ String fullyQualifiedXml = VIEWS_HEADER_BARE_PATTERN.matcher(xml).replaceFirst(VIEWS_HEADER_FQ);
+ try (InputStream xsd = MigratePortfoliosToNewTables.class.getResourceAsStream(SCHEMA_VIEWS)) {
+ InputSource viewsDefinition = new InputSource(new InputStreamReader(toInputStream(fullyQualifiedXml, UTF_8), UTF_8));
+
+ SchemaFactory saxSchemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+ saxSchemaFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
+ saxSchemaFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
+
+ SAXParserFactory parserFactory = SAXParserFactory.newInstance();
+ parserFactory.setFeature(FEATURE_SECURE_PROCESSING, true);
+ parserFactory.setNamespaceAware(true);
+ parserFactory.setSchema(saxSchemaFactory.newSchema(new SAXSource(new InputSource(xsd))));
+
+ SAXParser saxParser = parserFactory.newSAXParser();
+ saxParser.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
+ saxParser.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
+ saxParser.parse(viewsDefinition, new ViewsValidator());
+ }
+ }
+
+ private static List<ViewDef> parseViewDefinitions(SMInputCursor viewsCursor) throws XMLStreamException {
+ List<ViewDef> views = new ArrayList<>();
+ while (viewsCursor.getNext() != null) {
+ ViewDef viewDef = new ViewDef();
+ viewDef.setKey(viewsCursor.getAttrValue("key"));
+ viewDef.setDef(Boolean.parseBoolean(viewsCursor.getAttrValue("def")));
+ viewDef.setParent(viewsCursor.getAttrValue("parent"));
+ viewDef.setRoot(viewsCursor.getAttrValue("root"));
+ SMInputCursor viewCursor = viewsCursor.childElementCursor();
+ while (viewCursor.getNext() != null) {
+ String nodeName = viewCursor.getLocalName();
+ parseChildElement(viewDef, viewCursor, nodeName);
+ }
+ views.add(viewDef);
+ }
+ return views;
+ }
+
+ private static void parseChildElement(ViewDef viewDef, SMInputCursor viewCursor, String nodeName) throws XMLStreamException {
+ if (StringUtils.equals(nodeName, "name")) {
+ viewDef.setName(trim(viewCursor.collectDescendantText()));
+ } else if (StringUtils.equals(nodeName, "desc")) {
+ viewDef.setDesc(trim(viewCursor.collectDescendantText()));
+ } else if (StringUtils.equals(nodeName, "regexp")) {
+ viewDef.setRegexp(trim(viewCursor.collectDescendantText()));
+ } else if (StringUtils.equals(nodeName, "language")) {
+ viewDef.setLanguage(trim(viewCursor.collectDescendantText()));
+ } else if (StringUtils.equals(nodeName, "tag_key")) {
+ viewDef.setTagKey(trim(viewCursor.collectDescendantText()));
+ } else if (StringUtils.equals(nodeName, "tag_value")) {
+ viewDef.setTagValue(trim(viewCursor.collectDescendantText()));
+ } else if (StringUtils.equals(nodeName, "p")) {
+ viewDef.addProject(trim(viewCursor.collectDescendantText()));
+ } else if (StringUtils.equals(nodeName, "vw-ref")) {
+ viewDef.addReference(trim(viewCursor.collectDescendantText()));
+ } else if (StringUtils.equals(nodeName, "qualifier")) {
+ viewDef.setQualifier(trim(viewCursor.collectDescendantText()));
+ } else if (StringUtils.equals(nodeName, "tagsAssociation")) {
+ parseTagsAssociation(viewDef, viewCursor);
+ }
+ }
+
+ private static void parseTagsAssociation(ViewDef def, SMInputCursor viewCursor) throws XMLStreamException {
+ SMInputCursor projectCursor = viewCursor.childElementCursor();
+ while (projectCursor.getNext() != null) {
+ def.addTagAssociation(trim(projectCursor.collectDescendantText()));
+ }
+ }
+
+ private static SMInputFactory initStax() {
+ XMLInputFactory xmlFactory = XMLInputFactory.newInstance();
+ xmlFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE);
+ xmlFactory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, Boolean.FALSE);
+ // just so it won't try to load DTD in if there's DOCTYPE
+ xmlFactory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE);
+ xmlFactory.setProperty(XMLInputFactory.IS_VALIDATING, Boolean.FALSE);
+ return new SMInputFactory(xmlFactory);
+ }
+
+ static class ViewDef {
+ String key = null;
+
+ String parent = null;
+
+ String root = null;
+
+ boolean def = false;
+
+ List<String> p = new ArrayList<>();
+
+ List<String> vwRef = new ArrayList<>();
+
+ String name = null;
+
+ String desc = null;
+
+ String regexp = null;
+
+ String language = null;
+
+ String tagKey = null;
+
+ String tagValue = null;
+
+ String qualifier = null;
+
+ Set<String> tagsAssociation = new TreeSet<>();
+
+ public List<String> getProjects() {
+ return p;
+ }
+
+ public List<String> getReferences() {
+ return vwRef;
+ }
+
+ public ViewDef setKey(String key) {
+ this.key = key;
+ return this;
+ }
+
+ public ViewDef setParent(String parent) {
+ this.parent = parent;
+ return this;
+ }
+
+ public ViewDef setRoot(@Nullable String root) {
+ this.root = root;
+ return this;
+ }
+
+ public ViewDef setDef(boolean def) {
+ this.def = def;
+ return this;
+ }
+
+ public ViewDef addProject(String project) {
+ this.p.add(project);
+ return this;
+ }
+
+ public ViewDef setName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public ViewDef setDesc(@Nullable String desc) {
+ this.desc = desc;
+ return this;
+ }
+
+ public ViewDef setRegexp(@Nullable String regexp) {
+ this.regexp = regexp;
+ return this;
+ }
+
+ public ViewDef setLanguage(@Nullable String language) {
+ this.language = language;
+ return this;
+ }
+
+ public ViewDef setTagKey(@Nullable String tagKey) {
+ this.tagKey = tagKey;
+ return this;
+ }
+
+ public ViewDef setTagValue(@Nullable String tagValue) {
+ this.tagValue = tagValue;
+ return this;
+ }
+
+ public ViewDef addReference(String reference) {
+ this.vwRef.add(reference);
+ return this;
+ }
+
+ public ViewDef setQualifier(@Nullable String qualifier) {
+ this.qualifier = qualifier;
+ return this;
+ }
+
+ public ViewDef addTagAssociation(String tag) {
+ this.tagsAssociation.add(tag);
+ return this;
+ }
+ }
+
+ private static final class ViewsValidator extends DefaultHandler {
+ @Override
+ public void error(SAXParseException exception) throws SAXException {
+ throw exception;
+ }
+
+ @Override
+ public void warning(SAXParseException exception) throws SAXException {
+ throw exception;
+ }
+
+ @Override
+ public void fatalError(SAXParseException exception) throws SAXException {
+ throw exception;
+ }
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v91/DbVersion91Test.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v91/DbVersion91Test.java
index e5b51a680b6..0a361304f1b 100644
--- a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v91/DbVersion91Test.java
+++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v91/DbVersion91Test.java
@@ -41,7 +41,7 @@ public class DbVersion91Test {
@Test
public void verify_migration_count() {
- verifyMigrationCount(underTest, 13);
+ verifyMigrationCount(underTest, 14);
}
}
diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v91/MigratePortfoliosToNewTablesTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v91/MigratePortfoliosToNewTablesTest.java
new file mode 100644
index 00000000000..e8c8e48d9be
--- /dev/null
+++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v91/MigratePortfoliosToNewTablesTest.java
@@ -0,0 +1,431 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.version.v91;
+
+import java.sql.SQLException;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.impl.utils.TestSystem2;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.utils.System2;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.core.util.UuidFactoryFast;
+import org.sonar.db.CoreDbTester;
+import org.sonar.server.platform.db.migration.step.DataChange;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.tuple;
+
+public class MigratePortfoliosToNewTablesTest {
+ static final int TEXT_VALUE_MAX_LENGTH = 4000;
+ private static final long NOW = 10_000_000L;
+
+ @Rule
+ public final CoreDbTester db = CoreDbTester.createForSchema(MigratePortfoliosToNewTablesTest.class, "schema.sql");
+
+ private final UuidFactory uuidFactory = UuidFactoryFast.getInstance();
+ private final System2 system2 = new TestSystem2().setNow(NOW);
+
+ private final DataChange underTest = new MigratePortfoliosToNewTables(db.database(), uuidFactory, system2);
+
+ private final String SIMPLE_XML_ONE_PORTFOLIO = "<views>" +
+ "<vw key=\"port1\" def=\"false\">\n" +
+ " <name><![CDATA[name]]></name>\n" +
+ " <desc><![CDATA[description]]></desc>\n" +
+ " <qualifier><![CDATA[VW]]></qualifier>\n" +
+ " </vw>\n" +
+ "<vw key=\"port2\" def=\"false\">\n" +
+ " <name><![CDATA[name2]]></name>\n" +
+ " <desc><![CDATA[description2]]></desc>\n" +
+ " <qualifier><![CDATA[VW]]></qualifier>\n" +
+ " </vw>\n" +
+ "</views>";
+
+ private final String SIMPLE_XML_ONLY_PORTFOLIOS = "<views>" +
+ "<vw key=\"port1\" def=\"true\">\n" +
+ " <name><![CDATA[port1]]></name>\n" +
+ " <desc><![CDATA[]]></desc>\n" +
+ " <qualifier><![CDATA[VW]]></qualifier>\n" +
+ " </vw>\n" +
+ "<vw key=\"port3\" def=\"false\">\n" +
+ " <name><![CDATA[port3]]></name>\n" +
+ " <desc><![CDATA[]]></desc>\n" +
+ " <qualifier><![CDATA[VW]]></qualifier>\n" +
+ " </vw>\n" +
+ "<vw key=\"port2\" def=\"false\">\n" +
+ " <name><![CDATA[port2]]></name>\n" +
+ " <desc><![CDATA[]]></desc>\n" +
+ " <qualifier><![CDATA[VW]]></qualifier>\n" +
+ " <vw-ref><![CDATA[port1]]></vw-ref>\n" +
+ " <vw-ref><![CDATA[port3]]></vw-ref>\n" +
+ " </vw>\n" +
+ "</views>";
+
+ private final String SIMPLE_XML_PORTFOLIOS_AND_APP = "<views>" +
+ "<vw key=\"port1\" def=\"true\">\n" +
+ " <name><![CDATA[port1]]></name>\n" +
+ " <desc><![CDATA[]]></desc>\n" +
+ " <qualifier><![CDATA[VW]]></qualifier>\n" +
+ " </vw>\n" +
+ "<vw key=\"port3\" def=\"false\">\n" +
+ " <name><![CDATA[port3]]></name>\n" +
+ " <desc><![CDATA[]]></desc>\n" +
+ " <qualifier><![CDATA[VW]]></qualifier>\n" +
+ " </vw>\n" +
+ "<vw key=\"port2\" def=\"false\">\n" +
+ " <name><![CDATA[port2]]></name>\n" +
+ " <desc><![CDATA[]]></desc>\n" +
+ " <qualifier><![CDATA[VW]]></qualifier>\n" +
+ " <vw-ref><![CDATA[port1]]></vw-ref>\n" +
+ " <vw-ref><![CDATA[port3]]></vw-ref>\n" +
+ " </vw>\n" +
+ "<vw key=\"port4\" def=\"false\">\n" +
+ " <name><![CDATA[port2]]></name>\n" +
+ " <desc><![CDATA[]]></desc>\n" +
+ " <qualifier><![CDATA[VW]]></qualifier>\n" +
+ " <vw-ref><![CDATA[port2]]></vw-ref>\n" +
+ " <vw-ref><![CDATA[app1-key]]></vw-ref>\n" +
+ " <vw-ref><![CDATA[app2-key]]></vw-ref>\n" +
+ " <vw-ref><![CDATA[app3-key-not-existing]]></vw-ref>\n" +
+ " </vw>\n" +
+ "</views>";
+
+ private final String SIMPLE_XML_PORTFOLIOS_HIERARCHY = "<views>" +
+ " <vw key=\"port1\" def=\"false\">\n"
+ + " <name><![CDATA[port1]]></name>\n"
+ + " <desc><![CDATA[port1]]></desc>\n"
+ + " <qualifier><![CDATA[VW]]></qualifier>\n"
+ + " </vw>\n"
+ + " <vw key=\"port2\" def=\"false\" root=\"port1\" parent=\"port1\">\n"
+ + " <name><![CDATA[port2]]></name>\n"
+ + " <desc><![CDATA[port2]]></desc>\n"
+ + " </vw>\n"
+ + " <vw key=\"port3\" def=\"false\" root=\"port1\" parent=\"port1\">\n"
+ + " <name><![CDATA[port3]]></name>\n"
+ + " <desc><![CDATA[port3]]></desc>\n"
+ + " </vw>\n"
+ + " <vw key=\"port4\" def=\"false\" root=\"port1\" parent=\"port1\">\n"
+ + " <name><![CDATA[port4]]></name>\n"
+ + " <desc><![CDATA[port4]]></desc>\n"
+ + " </vw>\n"
+ + " <vw key=\"port5\" def=\"false\" root=\"port1\" parent=\"port2\">\n"
+ + " <name><![CDATA[port5]]></name>\n"
+ + " </vw>\n"
+ + " <vw key=\"port6\" def=\"false\" root=\"port1\" parent=\"port3\">\n"
+ + " <name><![CDATA[port6]]></name>\n"
+ + " <desc><![CDATA[port6]]></desc>\n"
+ + " </vw>\n"
+ + " <vw key=\"port7\" def=\"false\" root=\"port1\" parent=\"port3\">\n"
+ + " <name><![CDATA[port7]]></name>\n"
+ + " <desc><![CDATA[port7]]></desc>\n"
+ + " </vw>" +
+ "</views>";
+
+ private final String SIMPLE_XML_PORTFOLIOS_DIFFERENT_SELECTIONS = "<views>"
+ + " <vw key=\"port-regexp\" def=\"false\">\n"
+ + " <name><![CDATA[port-regexp]]></name>\n"
+ + " <desc><![CDATA[port-regexp]]></desc>\n"
+ + " <regexp><![CDATA[.*port.*]]></regexp>\n"
+ + " <qualifier><![CDATA[VW]]></qualifier>\n"
+ + " </vw>"
+ + " <vw key=\"port-tags\" def=\"false\">\n"
+ + " <name><![CDATA[port-tags]]></name>\n"
+ + " <desc><![CDATA[port-tags]]></desc>\n"
+ + " <qualifier><![CDATA[VW]]></qualifier>\n"
+ + " <tagsAssociation>\n"
+ + " <tag>tag1</tag>\n"
+ + " <tag>tag2</tag>\n"
+ + " <tag>tag3</tag>\n"
+ + " </tagsAssociation>\n"
+ + " </vw>"
+ + " <vw key=\"port-projects\" def=\"false\">\n"
+ + " <name><![CDATA[port-projects]]></name>\n"
+ + " <desc><![CDATA[port-projects]]></desc>\n"
+ + " <qualifier><![CDATA[VW]]></qualifier>\n"
+ + " <p>p1-key</p>\n"
+ + " <p>p2-key</p>\n"
+ + " <p>p3-key</p>\n"
+ + " <p>p4-key-not-existing</p>\n"
+ + " </vw>"
+ + "</views>";
+
+ private final String XML_PORTFOLIOS_LEGACY_SELECTIONS = "<views>"
+ + " <vw key=\"port-language\" def=\"false\">\n"
+ + " <name><![CDATA[port-language]]></name>\n"
+ + " <desc><![CDATA[port-language]]></desc>\n"
+ + " <language><![CDATA[javascript]]></language>\n"
+ + " <qualifier><![CDATA[VW]]></qualifier>\n"
+ + " </vw>"
+ + " <vw key=\"port-tag-value\" def=\"false\">\n"
+ + " <name><![CDATA[port-tag-value]]></name>\n"
+ + " <desc><![CDATA[port-tag-value]]></desc>\n"
+ + " <qualifier><![CDATA[VW]]></qualifier>\n"
+ + " <tag_key>tag-key</tag_key>\n"
+ + " <tag_value>tag-value</tag_value>\n"
+ + " </vw>"
+ + "</views>";
+
+ @Test
+ public void does_not_fail_when_nothing_to_migrate() throws SQLException {
+ assertThatCode(underTest::execute)
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ public void migrate_single_portfolio_with_default_visibility() throws SQLException {
+ insertViewsDefInternalProperty(SIMPLE_XML_ONE_PORTFOLIO);
+ insertDefaultVisibilityProperty(true);
+ insertComponent("uuid", "port2", false);
+
+ underTest.execute();
+ // re-entrant
+ underTest.execute();
+
+ assertThat(db.select("select kee, private, name, description, "
+ + "selection_mode, selection_expression, "
+ + "updated_at, created_at from portfolios"))
+ .extracting(
+ row -> row.get("KEE"),
+ row -> row.get("PRIVATE"),
+ row -> row.get("NAME"),
+ row -> row.get("DESCRIPTION"),
+ row -> row.get("SELECTION_MODE"),
+ row -> row.get("SELECTION_EXPRESSION"),
+ row -> row.get("UPDATED_AT"),
+ row -> row.get("CREATED_AT"))
+ .containsExactlyInAnyOrder(
+ tuple("port1", true, "name", "description", "NONE", null, NOW, NOW),
+ tuple("port2", false, "name2", "description2", "NONE", null, NOW, NOW));
+ }
+
+ @Test
+ public void migrate_portfolios_should_assign_component_uuid() throws SQLException {
+ insertViewsDefInternalProperty(SIMPLE_XML_ONE_PORTFOLIO);
+ insertComponent("uuid1", "port1", true);
+ insertComponent("uuid2", "port2", false);
+
+ underTest.execute();
+ // re-entrant
+ underTest.execute();
+
+ assertThat(db.select("select uuid, kee, private, name, description, "
+ + "selection_mode, selection_expression, "
+ + "updated_at, created_at from portfolios"))
+ .extracting(
+ row -> row.get("UUID"),
+ row -> row.get("KEE"),
+ row -> row.get("PRIVATE"),
+ row -> row.get("NAME"),
+ row -> row.get("DESCRIPTION"),
+ row -> row.get("SELECTION_MODE"),
+ row -> row.get("SELECTION_EXPRESSION"),
+ row -> row.get("UPDATED_AT"),
+ row -> row.get("CREATED_AT"))
+ .containsExactlyInAnyOrder(
+ tuple("uuid1", "port1", true, "name", "description", "NONE", null, NOW, NOW),
+ tuple("uuid2", "port2", false, "name2", "description2", "NONE", null, NOW, NOW));
+ }
+
+ @Test
+ public void migrate_simple_xml_only_portfolios_references() throws SQLException {
+ insertViewsDefInternalProperty(SIMPLE_XML_ONLY_PORTFOLIOS);
+
+ underTest.execute();
+ // re-entrant
+ underTest.execute();
+
+ assertThat(db.select("select kee from portfolios"))
+ .extracting(row -> row.get("KEE"))
+ .containsExactlyInAnyOrder("port1", "port2", "port3");
+
+ var portfolioKeyByUuid = db.select("select uuid, kee from portfolios").stream()
+ .collect(Collectors.toMap(row -> row.get("KEE"), row -> row.get("UUID").toString()));
+
+ String portfolio1Uuid = portfolioKeyByUuid.get("port1");
+ String portfolio2Uuid = portfolioKeyByUuid.get("port2");
+ String portfolio3Uuid = portfolioKeyByUuid.get("port3");
+
+ assertThat(db.select("select portfolio_uuid, reference_uuid from portfolio_references"))
+ .extracting(row -> row.get("PORTFOLIO_UUID"), row -> row.get("REFERENCE_UUID"))
+ .containsExactlyInAnyOrder(tuple(portfolio2Uuid, portfolio1Uuid), tuple(portfolio2Uuid, portfolio3Uuid));
+ }
+
+ @Test
+ public void migrate_simple_xml_portfolios_and_apps_references() throws SQLException {
+ insertViewsDefInternalProperty(SIMPLE_XML_PORTFOLIOS_AND_APP);
+
+ insertProject("app1-uuid", "app1-key", Qualifiers.APP);
+ insertProject("app2-uuid", "app2-key", Qualifiers.APP);
+ insertProject("proj1", "proj1-key", Qualifiers.PROJECT);
+
+ underTest.execute();
+ // re-entrant
+ underTest.execute();
+
+ assertThat(db.select("select kee from portfolios"))
+ .extracting(row -> row.get("KEE"))
+ .containsExactlyInAnyOrder("port1", "port2", "port3", "port4");
+
+ var portfolioKeyByUuid = db.select("select uuid, kee from portfolios").stream()
+ .collect(Collectors.toMap(row -> row.get("KEE"), row -> row.get("UUID").toString()));
+
+ String portfolio1Uuid = portfolioKeyByUuid.get("port1");
+ String portfolio2Uuid = portfolioKeyByUuid.get("port2");
+ String portfolio3Uuid = portfolioKeyByUuid.get("port3");
+ String portfolio4Uuid = portfolioKeyByUuid.get("port4");
+
+ assertThat(db.select("select portfolio_uuid, reference_uuid from portfolio_references"))
+ .extracting(row -> row.get("PORTFOLIO_UUID"), row -> row.get("REFERENCE_UUID"))
+ .containsExactlyInAnyOrder(
+ tuple(portfolio2Uuid, portfolio1Uuid),
+ tuple(portfolio2Uuid, portfolio3Uuid),
+ tuple(portfolio4Uuid, portfolio2Uuid),
+ tuple(portfolio4Uuid, "app1-uuid"),
+ tuple(portfolio4Uuid, "app2-uuid"))
+ .doesNotContain(tuple(portfolio4Uuid, "app3-key-not-existing"));
+ }
+
+ @Test
+ public void migrate_xml_portfolios_hierarchy() throws SQLException {
+ insertViewsDefInternalProperty(SIMPLE_XML_PORTFOLIOS_HIERARCHY);
+
+ underTest.execute();
+ // re-entrant
+ underTest.execute();
+
+ assertThat(db.select("select kee from portfolios"))
+ .extracting(row -> row.get("KEE"))
+ .containsExactlyInAnyOrder("port1", "port2", "port3", "port4", "port5", "port6", "port7");
+
+ var portfolioKeyByUuid = db.select("select uuid, kee from portfolios").stream()
+ .collect(Collectors.toMap(row -> row.get("KEE"), row -> row.get("UUID").toString()));
+
+ String portfolio1Uuid = portfolioKeyByUuid.get("port1");
+ String portfolio2Uuid = portfolioKeyByUuid.get("port2");
+ String portfolio3Uuid = portfolioKeyByUuid.get("port3");
+ String portfolio4Uuid = portfolioKeyByUuid.get("port4");
+ String portfolio5Uuid = portfolioKeyByUuid.get("port5");
+ String portfolio6Uuid = portfolioKeyByUuid.get("port6");
+ String portfolio7Uuid = portfolioKeyByUuid.get("port7");
+
+ assertThat(db.select("select uuid, parent_uuid, root_uuid from portfolios"))
+ .extracting(row -> row.get("UUID"), row -> row.get("PARENT_UUID"), row -> row.get("ROOT_UUID"))
+ .containsExactlyInAnyOrder(
+ tuple(portfolio1Uuid, null, portfolio1Uuid),
+ tuple(portfolio2Uuid, portfolio1Uuid, portfolio1Uuid),
+ tuple(portfolio3Uuid, portfolio1Uuid, portfolio1Uuid),
+ tuple(portfolio4Uuid, portfolio1Uuid, portfolio1Uuid),
+ tuple(portfolio5Uuid, portfolio2Uuid, portfolio1Uuid),
+ tuple(portfolio6Uuid, portfolio3Uuid, portfolio1Uuid),
+ tuple(portfolio7Uuid, portfolio3Uuid, portfolio1Uuid));
+ }
+
+ @Test
+ public void migrate_xml_portfolios_different_selections() throws SQLException {
+ insertViewsDefInternalProperty(SIMPLE_XML_PORTFOLIOS_DIFFERENT_SELECTIONS);
+
+ insertProject("p1-uuid", "p1-key", Qualifiers.PROJECT);
+ insertProject("p2-uuid", "p2-key", Qualifiers.PROJECT);
+ insertProject("p3-uuid", "p3-key", Qualifiers.PROJECT);
+
+ underTest.execute();
+ // re-entrant
+ underTest.execute();
+
+ assertThat(db.select("select kee, selection_mode, selection_expression from portfolios"))
+ .extracting(row -> row.get("KEE"), row -> row.get("SELECTION_MODE"), row -> row.get("SELECTION_EXPRESSION"))
+ .containsExactlyInAnyOrder(
+ tuple("port-regexp", "REGEXP", ".*port.*"),
+ tuple("port-projects", "MANUAL", null),
+ tuple("port-tags", "TAGS", "tag1,tag2,tag3"));
+
+ // verify projects
+ assertThat(db.select("select p.kee, pp.project_uuid from "
+ + "portfolio_projects pp join portfolios p on pp.portfolio_uuid = p.uuid where p.kee = 'port-projects'"))
+ .extracting(row -> row.get("KEE"), row -> row.get("PROJECT_UUID"))
+ .containsExactlyInAnyOrder(
+ tuple("port-projects", "p1-uuid"),
+ tuple("port-projects", "p2-uuid"),
+ tuple("port-projects", "p3-uuid"))
+ .doesNotContain(tuple("port-projects", "p4-uuid-not-existing"));
+ }
+
+ @Test
+ public void migrate_xml_portfolios_legacy_selections() throws SQLException {
+ insertViewsDefInternalProperty(XML_PORTFOLIOS_LEGACY_SELECTIONS);
+ underTest.execute();
+ // re-entrant
+ underTest.execute();
+
+ assertThat(db.select("select kee, selection_mode, selection_expression from portfolios"))
+ .extracting(row -> row.get("KEE"), row -> row.get("SELECTION_MODE"), row -> row.get("SELECTION_EXPRESSION"))
+ .containsExactlyInAnyOrder(
+ tuple("port-language", "NONE", null),
+ tuple("port-tag-value", "NONE", null));
+ }
+
+ private void insertProject(String uuid, String key, String qualifier) {
+ db.executeInsert("PROJECTS",
+ "UUID", uuid,
+ "KEE", key,
+ "QUALIFIER", qualifier,
+ "ORGANIZATION_UUID", uuid + "-key",
+ "TAGS", "tag1",
+ "PRIVATE", Boolean.toString(false),
+ "UPDATED_AT", System2.INSTANCE.now());
+ }
+
+ private void insertViewsDefInternalProperty(@Nullable String xml) {
+ String valueColumn = "text_value";
+ if (xml != null && xml.length() > TEXT_VALUE_MAX_LENGTH) {
+ valueColumn = "clob_value";
+ }
+
+ db.executeInsert("internal_properties",
+ "kee", "views.def",
+ "is_empty", "false",
+ valueColumn, xml,
+ "created_at", system2.now());
+ }
+
+ private void insertComponent(String uuid, String kee, boolean isPrivate) {
+ db.executeInsert("components",
+ "uuid", uuid,
+ "kee", kee,
+ "enabled", false,
+ "private", isPrivate,
+ "root_uuid", uuid,
+ "uuid_path", uuid,
+ "project_uuid", uuid);
+ }
+
+ private void insertDefaultVisibilityProperty(boolean isPrivate) {
+ db.executeInsert("properties",
+ "uuid", uuidFactory.create(),
+ "prop_key", "projects.default.visibility",
+ "IS_EMPTY", false,
+ "text_value", isPrivate ? "private" : "public",
+ "created_at", system2.now());
+ }
+
+}
diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v91/MigratePortfoliosToNewTablesTest/schema.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v91/MigratePortfoliosToNewTablesTest/schema.sql
new file mode 100644
index 00000000000..3cd223894dd
--- /dev/null
+++ b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v91/MigratePortfoliosToNewTablesTest/schema.sql
@@ -0,0 +1,110 @@
+CREATE TABLE "PROJECTS"(
+ "UUID" VARCHAR(40) NOT NULL,
+ "KEE" VARCHAR(400) NOT NULL,
+ "QUALIFIER" VARCHAR(10) NOT NULL,
+ "ORGANIZATION_UUID" VARCHAR(40) NOT NULL,
+ "NAME" VARCHAR(2000),
+ "DESCRIPTION" VARCHAR(2000),
+ "PRIVATE" BOOLEAN NOT NULL,
+ "TAGS" VARCHAR(500),
+ "CREATED_AT" BIGINT,
+ "UPDATED_AT" BIGINT NOT NULL
+);
+ALTER TABLE "PROJECTS" ADD CONSTRAINT "PK_NEW_PROJECTS" PRIMARY KEY("UUID");
+CREATE UNIQUE INDEX "UNIQ_PROJECTS_KEE" ON "PROJECTS"("KEE");
+CREATE INDEX "IDX_QUALIFIER" ON "PROJECTS"("QUALIFIER");
+
+CREATE TABLE "INTERNAL_PROPERTIES"(
+ "KEE" VARCHAR(20) NOT NULL,
+ "IS_EMPTY" BOOLEAN NOT NULL,
+ "TEXT_VALUE" VARCHAR(4000),
+ "CLOB_VALUE" CLOB,
+ "CREATED_AT" BIGINT NOT NULL
+);
+ALTER TABLE "INTERNAL_PROPERTIES" ADD CONSTRAINT "PK_INTERNAL_PROPERTIES" PRIMARY KEY("KEE");
+
+CREATE TABLE "PORTFOLIO_PROJECTS"(
+ "UUID" VARCHAR(40) NOT NULL,
+ "PORTFOLIO_UUID" VARCHAR(40) NOT NULL,
+ "PROJECT_UUID" VARCHAR(40) NOT NULL,
+ "CREATED_AT" BIGINT NOT NULL
+);
+ALTER TABLE "PORTFOLIO_PROJECTS" ADD CONSTRAINT "PK_PORTFOLIO_PROJECTS" PRIMARY KEY("UUID");
+
+CREATE TABLE "PORTFOLIO_REFERENCES"(
+ "UUID" VARCHAR(40) NOT NULL,
+ "PORTFOLIO_UUID" VARCHAR(40) NOT NULL,
+ "REFERENCE_UUID" VARCHAR(40) NOT NULL,
+ "CREATED_AT" BIGINT NOT NULL
+);
+ALTER TABLE "PORTFOLIO_REFERENCES" ADD CONSTRAINT "PK_PORTFOLIO_REFERENCES" PRIMARY KEY("UUID");
+
+CREATE TABLE "PORTFOLIOS"(
+ "UUID" VARCHAR(40) NOT NULL,
+ "KEE" VARCHAR(400) NOT NULL,
+ "NAME" VARCHAR(2000) NOT NULL,
+ "DESCRIPTION" VARCHAR(2000),
+ "ROOT_UUID" VARCHAR(40) NOT NULL,
+ "PARENT_UUID" VARCHAR(40),
+ "PRIVATE" BOOLEAN NOT NULL,
+ "SELECTION_MODE" VARCHAR(50) NOT NULL,
+ "SELECTION_EXPRESSION" VARCHAR(50),
+ "CREATED_AT" BIGINT NOT NULL,
+ "UPDATED_AT" BIGINT NOT NULL
+);
+ALTER TABLE "PORTFOLIOS" ADD CONSTRAINT "PK_PORTFOLIOS" PRIMARY KEY("UUID");
+
+CREATE TABLE "PROPERTIES"(
+ "UUID" VARCHAR(40) NOT NULL,
+ "PROP_KEY" VARCHAR(512) NOT NULL,
+ "IS_EMPTY" BOOLEAN NOT NULL,
+ "TEXT_VALUE" VARCHAR(4000),
+ "CLOB_VALUE" CLOB,
+ "CREATED_AT" BIGINT NOT NULL,
+ "COMPONENT_UUID" VARCHAR(40),
+ "USER_UUID" VARCHAR(255)
+);
+ALTER TABLE "PROPERTIES" ADD CONSTRAINT "PK_PROPERTIES" PRIMARY KEY("UUID");
+CREATE INDEX "PROPERTIES_KEY" ON "PROPERTIES"("PROP_KEY");
+
+CREATE TABLE "COMPONENTS"(
+ "UUID" VARCHAR(50) NOT NULL,
+ "KEE" VARCHAR(400),
+ "DEPRECATED_KEE" VARCHAR(400),
+ "NAME" VARCHAR(2000),
+ "LONG_NAME" VARCHAR(2000),
+ "DESCRIPTION" VARCHAR(2000),
+ "ENABLED" BOOLEAN DEFAULT TRUE NOT NULL,
+ "SCOPE" VARCHAR(3),
+ "QUALIFIER" VARCHAR(10),
+ "PRIVATE" BOOLEAN NOT NULL,
+ "ROOT_UUID" VARCHAR(50) NOT NULL,
+ "LANGUAGE" VARCHAR(20),
+ "COPY_COMPONENT_UUID" VARCHAR(50),
+ "PATH" VARCHAR(2000),
+ "UUID_PATH" VARCHAR(1500) NOT NULL,
+ "PROJECT_UUID" VARCHAR(50) NOT NULL,
+ "MODULE_UUID" VARCHAR(50),
+ "MODULE_UUID_PATH" VARCHAR(1500),
+ "MAIN_BRANCH_PROJECT_UUID" VARCHAR(50),
+ "B_CHANGED" BOOLEAN,
+ "B_NAME" VARCHAR(500),
+ "B_LONG_NAME" VARCHAR(500),
+ "B_DESCRIPTION" VARCHAR(2000),
+ "B_ENABLED" BOOLEAN,
+ "B_QUALIFIER" VARCHAR(10),
+ "B_LANGUAGE" VARCHAR(20),
+ "B_COPY_COMPONENT_UUID" VARCHAR(50),
+ "B_PATH" VARCHAR(2000),
+ "B_UUID_PATH" VARCHAR(1500),
+ "B_MODULE_UUID" VARCHAR(50),
+ "B_MODULE_UUID_PATH" VARCHAR(1500),
+ "CREATED_AT" TIMESTAMP
+);
+CREATE UNIQUE INDEX "PROJECTS_KEE" ON "COMPONENTS"("KEE");
+CREATE INDEX "PROJECTS_MODULE_UUID" ON "COMPONENTS"("MODULE_UUID");
+CREATE INDEX "PROJECTS_PROJECT_UUID" ON "COMPONENTS"("PROJECT_UUID");
+CREATE INDEX "PROJECTS_QUALIFIER" ON "COMPONENTS"("QUALIFIER");
+CREATE INDEX "PROJECTS_ROOT_UUID" ON "COMPONENTS"("ROOT_UUID");
+CREATE INDEX "PROJECTS_UUID" ON "COMPONENTS"("UUID");
+CREATE INDEX "IDX_MAIN_BRANCH_PRJ_UUID" ON "COMPONENTS"("MAIN_BRANCH_PROJECT_UUID");