diff options
author | Sébastien Lesaint <sebastien.lesaint@sonarsource.com> | 2019-07-26 17:16:59 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2019-08-09 20:21:24 +0200 |
commit | a5c755840083007e6531254cc9d7f6e566b250fd (patch) | |
tree | b5b0ec975ca4dda169d4719063e5aca2aa2ed7a4 /server/sonar-db-core | |
parent | ba3c8baa182f43e6e848ed2f503e4f260ce0aa1c (diff) | |
download | sonarqube-a5c755840083007e6531254cc9d7f6e566b250fd.tar.gz sonarqube-a5c755840083007e6531254cc9d7f6e566b250fd.zip |
remove DBUnit
Diffstat (limited to 'server/sonar-db-core')
7 files changed, 70 insertions, 424 deletions
diff --git a/server/sonar-db-core/build.gradle b/server/sonar-db-core/build.gradle index ed70fcca8ea..e7e7485f052 100644 --- a/server/sonar-db-core/build.gradle +++ b/server/sonar-db-core/build.gradle @@ -30,7 +30,6 @@ dependencies { testCompile 'com.tngtech.java:junit-dataprovider' testCompile 'junit:junit' testCompile 'org.assertj:assertj-core' - testCompile 'org.dbunit:dbunit' testCompile 'org.mockito:mockito-core' testCompile 'org.postgresql:postgresql' testCompile project(':sonar-testing-harness') diff --git a/server/sonar-db-core/src/test/java/org/sonar/db/AbstractDbTester.java b/server/sonar-db-core/src/test/java/org/sonar/db/AbstractDbTester.java index 2d3c7df948d..0b9bb16bc6e 100644 --- a/server/sonar-db-core/src/test/java/org/sonar/db/AbstractDbTester.java +++ b/server/sonar-db-core/src/test/java/org/sonar/db/AbstractDbTester.java @@ -24,7 +24,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Ordering; -import java.io.InputStream; import java.math.BigDecimal; import java.sql.Clob; import java.sql.Connection; @@ -49,20 +48,6 @@ import javax.annotation.Nullable; import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; -import org.dbunit.Assertion; -import org.dbunit.DatabaseUnitException; -import org.dbunit.assertion.DiffCollectingFailureHandler; -import org.dbunit.assertion.Difference; -import org.dbunit.database.DatabaseConfig; -import org.dbunit.database.IDatabaseConnection; -import org.dbunit.dataset.CompositeDataSet; -import org.dbunit.dataset.IDataSet; -import org.dbunit.dataset.ITable; -import org.dbunit.dataset.ReplacementDataSet; -import org.dbunit.dataset.filter.DefaultColumnFilter; -import org.dbunit.dataset.xml.FlatXmlDataSet; -import org.dbunit.ext.mssql.InsertIdentityOperation; -import org.dbunit.operation.DatabaseOperation; import org.junit.rules.ExternalResource; import org.sonar.api.utils.log.Loggers; import org.sonar.core.util.stream.MoreCollectors; @@ -147,7 +132,7 @@ public class AbstractDbTester<T extends TestDb> extends ExternalResource { } String sql = "insert into " + table.toLowerCase(Locale.ENGLISH) + " (" + - COMMA_JOINER.join(valuesByColumn.keySet()) + + COMMA_JOINER.join(valuesByColumn.keySet().stream().map(t -> t.toLowerCase(Locale.ENGLISH)).toArray(String[]::new)) + ") values (" + COMMA_JOINER.join(Collections.nCopies(valuesByColumn.size(), '?')) + ")"; @@ -257,124 +242,6 @@ public class AbstractDbTester<T extends TestDb> extends ExternalResource { return rows; } - /** - * @deprecated do not use DBUnit - */ - @Deprecated - public void prepareDbUnit(Class testClass, String... testNames) { - InputStream[] streams = new InputStream[testNames.length]; - try { - for (int i = 0; i < testNames.length; i++) { - String path = "/" + testClass.getName().replace('.', '/') + "/" + testNames[i]; - streams[i] = testClass.getResourceAsStream(path); - if (streams[i] == null) { - throw new IllegalStateException("DbUnit file not found: " + path); - } - } - - prepareDbUnit(streams); - db.getCommands().resetPrimaryKeys(db.getDatabase().getDataSource()); - } catch (SQLException e) { - throw translateException("Could not setup DBUnit data", e); - } finally { - for (InputStream stream : streams) { - IOUtils.closeQuietly(stream); - } - } - } - - private void prepareDbUnit(InputStream... dataSetStream) { - IDatabaseConnection connection = null; - try { - IDataSet[] dataSets = new IDataSet[dataSetStream.length]; - for (int i = 0; i < dataSetStream.length; i++) { - dataSets[i] = dbUnitDataSet(dataSetStream[i]); - } - db.getDbUnitTester().setDataSet(new CompositeDataSet(dataSets)); - connection = dbUnitConnection(); - new InsertIdentityOperation(DatabaseOperation.INSERT).execute(connection, db.getDbUnitTester().getDataSet()); - } catch (Exception e) { - throw translateException("Could not setup DBUnit data", e); - } finally { - closeQuietly(connection); - } - } - - /** - * @deprecated do not use DBUnit - */ - @Deprecated - public void assertDbUnitTable(Class testClass, String filename, String table, String... columns) { - IDatabaseConnection connection = dbUnitConnection(); - try { - IDataSet dataSet = connection.createDataSet(); - String path = "/" + testClass.getName().replace('.', '/') + "/" + filename; - IDataSet expectedDataSet = dbUnitDataSet(testClass.getResourceAsStream(path)); - ITable filteredTable = DefaultColumnFilter.includedColumnsTable(dataSet.getTable(table), columns); - ITable filteredExpectedTable = DefaultColumnFilter.includedColumnsTable(expectedDataSet.getTable(table), columns); - Assertion.assertEquals(filteredExpectedTable, filteredTable); - } catch (DatabaseUnitException e) { - fail(e.getMessage()); - } catch (SQLException e) { - throw translateException("Error while checking results", e); - } finally { - closeQuietly(connection); - } - } - - /** - * @deprecated do not use DBUnit - */ - @Deprecated - public void assertDbUnit(Class testClass, String filename, String... tables) { - assertDbUnit(testClass, filename, new String[0], tables); - } - - /** - * @deprecated do not use DBUnit - */ - @Deprecated - public void assertDbUnit(Class testClass, String filename, String[] excludedColumnNames, String... tables) { - IDatabaseConnection connection = null; - try { - connection = dbUnitConnection(); - - IDataSet dataSet = connection.createDataSet(); - String path = "/" + testClass.getName().replace('.', '/') + "/" + filename; - InputStream inputStream = testClass.getResourceAsStream(path); - if (inputStream == null) { - throw new IllegalStateException(String.format("File '%s' does not exist", path)); - } - IDataSet expectedDataSet = dbUnitDataSet(inputStream); - for (String table : tables) { - DiffCollectingFailureHandler diffHandler = new DiffCollectingFailureHandler(); - - ITable filteredTable = DefaultColumnFilter.excludedColumnsTable(dataSet.getTable(table), excludedColumnNames); - ITable filteredExpectedTable = DefaultColumnFilter.excludedColumnsTable(expectedDataSet.getTable(table), excludedColumnNames); - Assertion.assertEquals(filteredExpectedTable, filteredTable, diffHandler); - // Evaluate the differences and ignore some column values - List diffList = diffHandler.getDiffList(); - for (Object o : diffList) { - Difference diff = (Difference) o; - if (!"[ignore]".equals(diff.getExpectedValue())) { - throw new DatabaseUnitException(diff.toString()); - } - } - } - } catch (DatabaseUnitException e) { - e.printStackTrace(); - fail(e.getMessage()); - } catch (Exception e) { - throw translateException("Error while checking results", e); - } finally { - closeQuietly(connection); - } - } - - public void assertColumnDefinition(String table, String column, int expectedType, @Nullable Integer expectedSize) { - assertColumnDefinition(table, column, expectedType, expectedSize, null); - } - public void assertColumnDefinition(String table, String column, int expectedType, @Nullable Integer expectedSize, @Nullable Boolean isNullable) { try (Connection connection = getConnection(); PreparedStatement stmt = connection.prepareStatement("select * from " + table); @@ -569,35 +436,6 @@ public class AbstractDbTester<T extends TestDb> extends ExternalResource { } } - private IDataSet dbUnitDataSet(InputStream stream) { - try { - ReplacementDataSet dataSet = new ReplacementDataSet(new FlatXmlDataSet(stream)); - dataSet.addReplacementObject("[null]", null); - dataSet.addReplacementObject("[false]", Boolean.FALSE); - dataSet.addReplacementObject("[true]", Boolean.TRUE); - - return dataSet; - } catch (Exception e) { - throw translateException("Could not read the dataset stream", e); - } - } - - private IDatabaseConnection dbUnitConnection() { - try { - IDatabaseConnection connection = db.getDbUnitTester().getConnection(); - connection.getConfig().setProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY, db.getDbUnitFactory()); - return connection; - } catch (Exception e) { - throw translateException("Error while getting connection", e); - } - } - - public static RuntimeException translateException(String msg, Exception cause) { - RuntimeException runtimeException = new RuntimeException(String.format("%s: [%s] %s", msg, cause.getClass().getName(), cause.getMessage())); - runtimeException.setStackTrace(cause.getStackTrace()); - return runtimeException; - } - private static void doClobFree(Clob clob) throws SQLException { try { clob.free(); @@ -606,16 +444,6 @@ public class AbstractDbTester<T extends TestDb> extends ExternalResource { } } - private void closeQuietly(@Nullable IDatabaseConnection connection) { - try { - if (connection != null) { - connection.close(); - } - } catch (SQLException e) { - // ignore - } - } - public Connection openConnection() throws SQLException { return getConnection(); } @@ -628,10 +456,6 @@ public class AbstractDbTester<T extends TestDb> extends ExternalResource { return db.getDatabase(); } - public DatabaseCommands getCommands() { - return db.getCommands(); - } - /** * An {@link AutoCloseable} supplier of {@link Connection}. */ diff --git a/server/sonar-db-core/src/test/java/org/sonar/db/CoreTestDb.java b/server/sonar-db-core/src/test/java/org/sonar/db/CoreTestDb.java index 281c7fa5225..4479031f8c3 100644 --- a/server/sonar-db-core/src/test/java/org/sonar/db/CoreTestDb.java +++ b/server/sonar-db-core/src/test/java/org/sonar/db/CoreTestDb.java @@ -19,18 +19,22 @@ */ package org.sonar.db; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; import javax.annotation.Nullable; +import javax.sql.DataSource; import org.apache.commons.codec.digest.DigestUtils; -import org.dbunit.DataSourceDatabaseTester; -import org.dbunit.IDatabaseTester; import org.junit.AssumptionViolatedException; import org.sonar.api.config.Settings; import org.sonar.api.config.internal.MapSettings; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; +import org.sonar.db.version.SqTables; import static java.util.Objects.requireNonNull; import static org.sonar.process.ProcessProperties.Property.JDBC_USERNAME; @@ -49,17 +53,13 @@ import static org.sonar.process.ProcessProperties.Property.JDBC_USERNAME; class CoreTestDb implements TestDb { private Database db; - private DatabaseCommands commands; - private IDatabaseTester tester; protected CoreTestDb() { // use static factory method } - protected CoreTestDb(Database db, DatabaseCommands commands, IDatabaseTester tester) { + protected CoreTestDb(Database db) { this.db = db; - this.commands = commands; - this.tester = tester; } static CoreTestDb create(String schemaPath) { @@ -113,9 +113,7 @@ class CoreTestDb implements TestDb { databaseInitializer.accept(db); Loggers.get(getClass()).debug("Test Database: " + db); - commands = DatabaseCommands.forDialect(db.getDialect()); String login = settings.getString(JDBC_USERNAME.getKey()); - tester = new DataSourceDatabaseTester(db.getDataSource(), commands.useLoginAsSchema() ? login : null); extendedStart.accept(db, true); } else { @@ -123,19 +121,53 @@ class CoreTestDb implements TestDb { } } - @Override - public Database getDatabase() { - return db; + public void truncateTables() { + try { + truncateDatabase(getDatabase().getDataSource()); + } catch (SQLException e) { + throw new IllegalStateException("Fail to truncate db tables", e); + } } - @Override - public DatabaseCommands getCommands() { - return commands; + private void truncateDatabase(DataSource dataSource) throws SQLException { + try (Connection connection = dataSource.getConnection()) { + connection.setAutoCommit(false); + try (Statement statement = connection.createStatement()) { + for (String table : SqTables.TABLES) { + try { + if (shouldTruncate(connection, table)) { + statement.executeUpdate(truncateSql(table)); + connection.commit(); + } + } catch (Exception e) { + connection.rollback(); + throw new IllegalStateException("Fail to truncate table " + table, e); + } + } + } + } + } + + private static boolean shouldTruncate(Connection connection, String table) { + try (Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("select count(1) from " + table)) { + if (rs.next()) { + return rs.getInt(1) > 0; + } + + } catch (SQLException ignored) { + // probably because table does not exist. That's the case with H2 tests. + } + return false; + } + + private static String truncateSql(String table) { + return "TRUNCATE TABLE " + table; } @Override - public IDatabaseTester getDbUnitTester() { - return tester; + public Database getDatabase() { + return db; } @Override diff --git a/server/sonar-db-core/src/test/java/org/sonar/db/DatabaseCommands.java b/server/sonar-db-core/src/test/java/org/sonar/db/DatabaseCommands.java deleted file mode 100644 index 17824d95bd4..00000000000 --- a/server/sonar-db-core/src/test/java/org/sonar/db/DatabaseCommands.java +++ /dev/null @@ -1,201 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2019 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.db; - -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableMap; -import java.sql.Connection; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.Arrays; -import java.util.List; -import javax.sql.DataSource; -import org.apache.commons.dbutils.DbUtils; -import org.apache.commons.lang.StringUtils; -import org.dbunit.dataset.datatype.DefaultDataTypeFactory; -import org.dbunit.dataset.datatype.IDataTypeFactory; -import org.dbunit.dataset.datatype.ToleratedDeltaMap; -import org.dbunit.ext.h2.H2DataTypeFactory; -import org.dbunit.ext.mssql.MsSqlDataTypeFactory; -import org.dbunit.ext.oracle.Oracle10DataTypeFactory; -import org.dbunit.ext.postgresql.PostgresqlDataTypeFactory; -import org.sonar.db.dialect.Dialect; -import org.sonar.db.dialect.MsSql; -import org.sonar.db.dialect.Oracle; -import org.sonar.db.dialect.PostgreSql; -import org.sonar.db.version.SqTables; - -public abstract class DatabaseCommands { - private final IDataTypeFactory dbUnitFactory; - - private DatabaseCommands(DefaultDataTypeFactory dbUnitFactory) { - this.dbUnitFactory = dbUnitFactory; - - // Hack for MsSQL failure in IssueMapperTest. - // All the Double fields should be listed here. - dbUnitFactory.addToleratedDelta(new ToleratedDeltaMap.ToleratedDelta("issues", "effort_to_fix", 0.0001)); - } - - public final IDataTypeFactory getDbUnitFactory() { - return dbUnitFactory; - } - - abstract List<String> resetSequenceSql(String table, int minSequenceValue); - - String truncateSql(String table) { - return "TRUNCATE TABLE " + table; - } - - boolean useLoginAsSchema() { - return false; - } - - public static DatabaseCommands forDialect(Dialect dialect) { - DatabaseCommands command = ImmutableMap.of( - org.sonar.db.dialect.H2.ID, H2, - MsSql.ID, MSSQL, - Oracle.ID, ORACLE, - PostgreSql.ID, POSTGRESQL).get(dialect.getId()); - - return Preconditions.checkNotNull(command, "Unknown database: " + dialect); - } - - private static final DatabaseCommands H2 = new DatabaseCommands(new H2DataTypeFactory()) { - @Override - List<String> resetSequenceSql(String table, int minSequenceValue) { - return Arrays.asList("ALTER TABLE " + table + " ALTER COLUMN ID RESTART WITH " + minSequenceValue); - } - }; - - private static final DatabaseCommands POSTGRESQL = new DatabaseCommands(new PostgresqlDataTypeFactory()) { - @Override - List<String> resetSequenceSql(String table, int minSequenceValue) { - return Arrays.asList("ALTER SEQUENCE " + table + "_id_seq RESTART WITH " + minSequenceValue); - } - }; - - private static final DatabaseCommands ORACLE = new DatabaseCommands(new Oracle10DataTypeFactory()) { - @Override - List<String> resetSequenceSql(String table, int minSequenceValue) { - String sequence = StringUtils.upperCase(table) + "_SEQ"; - return Arrays.asList( - "DROP SEQUENCE " + sequence, - "CREATE SEQUENCE " + sequence + " INCREMENT BY 1 MINVALUE 1 START WITH " + minSequenceValue); - } - - @Override - String truncateSql(String table) { - return "DELETE FROM " + table; - } - - @Override - boolean useLoginAsSchema() { - return true; - } - }; - - private static final DatabaseCommands MSSQL = new DatabaseCommands(new MsSqlDataTypeFactory()) { - @Override - public void resetPrimaryKeys(DataSource dataSource) { - } - - @Override - List<String> resetSequenceSql(String table, int minSequenceValue) { - return null; - } - - @Override - protected boolean shouldTruncate(Connection connection, String table) { - // truncate all tables on mssql, else unexpected errors in some tests - return true; - } - }; - - public void truncateDatabase(DataSource dataSource) throws SQLException { - Connection connection = dataSource.getConnection(); - Statement statement = null; - try { - connection.setAutoCommit(false); - statement = connection.createStatement(); - for (String table : SqTables.TABLES) { - try { - if (shouldTruncate(connection, table)) { - statement.executeUpdate(truncateSql(table)); - connection.commit(); - } - } catch (Exception e) { - connection.rollback(); - throw new IllegalStateException("Fail to truncate table " + table, e); - } - } - } finally { - DbUtils.closeQuietly(connection); - DbUtils.closeQuietly(statement); - } - } - - protected boolean shouldTruncate(Connection connection, String table) throws SQLException { - Statement stmt = connection.createStatement(); - ResultSet rs = null; - try { - rs = stmt.executeQuery("select count(1) from " + table); - if (rs.next()) { - return rs.getInt(1) > 0; - } - - } catch (SQLException ignored) { - // probably because table does not exist. That's the case with H2 tests. - } finally { - DbUtils.closeQuietly(rs); - DbUtils.closeQuietly(stmt); - } - return false; - } - - public void resetPrimaryKeys(DataSource dataSource) throws SQLException { - Connection connection = null; - Statement statement = null; - ResultSet resultSet = null; - try { - connection = dataSource.getConnection(); - connection.setAutoCommit(false); - - statement = connection.createStatement(); - for (String table : SqTables.TABLES) { - try { - resultSet = statement.executeQuery("SELECT CASE WHEN MAX(ID) IS NULL THEN 1 ELSE MAX(ID)+1 END FROM " + table); - resultSet.next(); - int maxId = resultSet.getInt(1); - resultSet.close(); - - for (String resetCommand : resetSequenceSql(table, maxId)) { - statement.executeUpdate(resetCommand); - } - connection.commit(); - } catch (Exception e) { - connection.rollback(); // this table has no primary key - } - } - } finally { - DbUtils.closeQuietly(connection, statement, resultSet); - } - } -} diff --git a/server/sonar-db-core/src/test/java/org/sonar/db/ResultSetIteratorTest.java b/server/sonar-db-core/src/test/java/org/sonar/db/ResultSetIteratorTest.java index 75b5e729608..5b8b5bcbb06 100644 --- a/server/sonar-db-core/src/test/java/org/sonar/db/ResultSetIteratorTest.java +++ b/server/sonar-db-core/src/test/java/org/sonar/db/ResultSetIteratorTest.java @@ -38,7 +38,9 @@ public class ResultSetIteratorTest { @Test public void create_iterator_from_statement() throws Exception { - dbTester.prepareDbUnit(getClass(), "feed.xml"); + insert(10, "AB"); + insert(20, "AB"); + insert(30, "AB"); try (Connection connection = dbTester.openConnection()) { PreparedStatement stmt = connection.prepareStatement("select * from issues order by id"); @@ -72,7 +74,9 @@ public class ResultSetIteratorTest { @Test public void iterate_empty_list() throws Exception { - dbTester.prepareDbUnit(getClass(), "feed.xml"); + insert(10, "AB"); + insert(20, "AB"); + insert(30, "AB"); try (Connection connection = dbTester.openConnection()) { PreparedStatement stmt = connection.prepareStatement("select * from issues where id < 0"); @@ -84,7 +88,9 @@ public class ResultSetIteratorTest { @Test public void create_iterator_from_result_set() throws Exception { - dbTester.prepareDbUnit(getClass(), "feed.xml"); + insert(10, "AB"); + insert(20, "AB"); + insert(30, "AB"); try (Connection connection = dbTester.openConnection()) { PreparedStatement stmt = connection.prepareStatement("select * from issues order by id"); @@ -120,7 +126,9 @@ public class ResultSetIteratorTest { @Test public void fail_to_read_row() throws Exception { - dbTester.prepareDbUnit(getClass(), "feed.xml"); + insert(10, "AB"); + insert(20, "AB"); + insert(30, "AB"); try (Connection connection = dbTester.openConnection()) { PreparedStatement stmt = connection.prepareStatement("select * from issues order by id"); @@ -165,4 +173,12 @@ public class ResultSetIteratorTest { return rs.getInt(1234); } } + + private void insert(int id, String key) { + dbTester.executeInsert( + "ISSUES", + "ID", id, + "KEE", key + ); + } } diff --git a/server/sonar-db-core/src/test/java/org/sonar/db/TestDb.java b/server/sonar-db-core/src/test/java/org/sonar/db/TestDb.java index 22645af7787..c87e3715c12 100644 --- a/server/sonar-db-core/src/test/java/org/sonar/db/TestDb.java +++ b/server/sonar-db-core/src/test/java/org/sonar/db/TestDb.java @@ -19,10 +19,6 @@ */ package org.sonar.db; -import java.sql.SQLException; -import org.dbunit.IDatabaseTester; -import org.dbunit.dataset.datatype.IDataTypeFactory; - public interface TestDb { void start(); @@ -30,19 +26,4 @@ public interface TestDb { Database getDatabase(); - DatabaseCommands getCommands(); - - IDatabaseTester getDbUnitTester(); - - default void truncateTables() { - try { - getCommands().truncateDatabase(getDatabase().getDataSource()); - } catch (SQLException e) { - throw new IllegalStateException("Fail to truncate db tables", e); - } - } - - default IDataTypeFactory getDbUnitFactory() { - return getCommands().getDbUnitFactory(); - } } diff --git a/server/sonar-db-core/src/test/resources/org/sonar/db/ResultSetIteratorTest/feed.xml b/server/sonar-db-core/src/test/resources/org/sonar/db/ResultSetIteratorTest/feed.xml deleted file mode 100644 index e76d538413e..00000000000 --- a/server/sonar-db-core/src/test/resources/org/sonar/db/ResultSetIteratorTest/feed.xml +++ /dev/null @@ -1,5 +0,0 @@ -<dataset> - <issues id="10" kee="AB" /> - <issues id="20" kee="CD" /> - <issues id="30" kee="EF" /> -</dataset> |