Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[vividus] Add left join table transformer #5285

Merged
merged 1 commit into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 46 additions & 21 deletions docs/modules/commons/pages/table-transformers.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -675,34 +675,59 @@ Examples:
|value2|0 |
----

== INNER_JOIN
== LEFT_JOIN

`INNER_JOIN` transformer combines rows from two tables whenever there are matching values between the columns.
The `LEFT_JOIN` transformer combines all rows from the left table with matching rows from the right table, and includes rows from the left table even if there are no matches in the right table.

[cols="1,3", options="header"]
|===
|Parameter
|Description
include::partial$join-transformer.adoc[]

|`leftTableJoinColumn`
|the column name for matching in the left table
.Usage of LEFT_JOIN transformer with two tables
[source,gherkin]
----
{transformer=LEFT_JOIN, leftTableJoinColumn=ID, rightTableJoinColumn=ID, tables=/tables/customers.table;/tables/orders.table}
----

|`rightTableJoinColumn`
|the column name for matching in the right table
where xref:ROOT:glossary.adoc#_examplestable[ExamplesTable] from /tables/customers.table:

|`tables`
|xref:ROOT:glossary.adoc#_examplestable[ExamplesTable]-s to join
|===
[source,gherkin]
----
|ID|Customer Name|Country |
|1 |Alice |USA |
|2 |Bob |Canada |
|3 |Charlie |UK |
|4 |David |Australia|
|5 |Eva |Germany |
----

- `left` table is the first xref:ROOT:glossary.adoc#_examplestable[ExamplesTable] declared in `tables` parameter,
- `right` table is the second xref:ROOT:glossary.adoc#_examplestable[ExamplesTable] declared in `tables` parameter or table body put under the transformer definition.
and xref:ROOT:glossary.adoc#_examplestable[ExamplesTable] from /tables/orders.table:

[IMPORTANT]
====
* The number of used xref:ROOT:glossary.adoc#_examplestable[ExamplesTable]-s must be equal to 2 (`left` and `right`).
* The column names of input tables must be different (except the column names for matching).
* If any of tables is empty - the resulting table will also be empty.
====
[source,gherkin]
----
|ID|Order ID|Order Amount|
|1 |101 |150.00 |
|3 |102 |200.00 |
|5 |103 |250.00 |
|5 |104 |300.00 |
----

will result in the following xref:ROOT:glossary.adoc#_examplestable[ExamplesTable]:

[source,gherkin]
----
|Country |ID|Order Amount|Customer Name|Order ID|
|USA |1 |150.00 |Alice |101 |
|Canada |2 | |Bob | |
|UK |3 |200.00 |Charlie |102 |
|Australia|4 |David | | |
|Germany |5 |250.00 |Eva |103 |
|Germany |5 |300.00 |Eva |104 |
----

== INNER_JOIN

`INNER_JOIN` transformer combines rows from two tables whenever there are matching values between the columns.

include::partial$join-transformer.adoc[]

.Usage of INNER_JOIN transformer with table body
[source,gherkin]
Expand Down
24 changes: 24 additions & 0 deletions docs/modules/commons/partials/join-transformer.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[cols="1,3", options="header"]
|===
|Parameter
|Description

|`leftTableJoinColumn`
|the column name for matching in the left table

|`rightTableJoinColumn`
|the column name for matching in the right table

|`tables`
|xref:ROOT:glossary.adoc#_examplestable[ExamplesTable]-s to join
|===

- `left` table is the first xref:ROOT:glossary.adoc#_examplestable[ExamplesTable] declared in `tables` parameter,
- `right` table is the second xref:ROOT:glossary.adoc#_examplestable[ExamplesTable] declared in `tables` parameter or table body put under the transformer definition.

[IMPORTANT]
====
* The number of used xref:ROOT:glossary.adoc#_examplestable[ExamplesTable]-s must be equal to 2 (`left` and `right`).
* The column names of input tables must be different (except the column names for matching).
* If any of tables is empty - the resulting table will also be empty.
====
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,21 @@ Then `${innerJoinTable}` is equal to table:
|row133 |3 |row53 |row43 |row333 |row233 |
|row133 |3 |row533 |row433 |row333 |row233 |

Scenario: Verify LEFT_JOIN transformer with table body
When I initialize scenario variable `leftJoinTable` with values:
{transformer=LEFT_JOIN, leftTableJoinColumn=joinID, rightTableJoinColumn=joinID, tables=/data/for-inner-join-transformer.table}
|joinID|column4|column5|
|5 |row45 |row51 |
|1 |row41 |row51 |
|6 |row41 |row51 |
Then `${leftJoinTable}` is equal to table:
|column1|joinID|column5|column4|column3|column2|
|row11 |1 |row51 |row41 |row31 |row21 |
|row12 |2 | | |row32 |row22 |
|row13 |3 | | |row33 |row23 |
|row133 |3 | | |row333 |row233 |
|row14 |4 | | |row34 |row24 |

Scenario: Verify SORTING transformer with default order
When I initialize scenario variable `sortingTable` with values:
{transformer=SORTING, byColumns=key1|key2}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright 2019-2024 the original author or authors.
*
* 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
*
* https://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.vividus.transformer;

import static org.apache.commons.lang3.Validate.isTrue;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.jbehave.core.model.ExamplesTable;
import org.jbehave.core.model.ExamplesTable.TableProperties;
import org.jbehave.core.model.TableParsers;
import org.vividus.util.ExamplesTableProcessor;

public abstract class AbstractJoinTableTransformer extends AbstractTableLoadingTransformer
{
protected AbstractJoinTableTransformer()
{
super(false);
}

@Override
public String transform(String tableAsString, TableParsers tableParsers, TableProperties properties)
{
List<ExamplesTable> tables = loadTables(tableAsString, properties);
isTrue(tables.size() == 2, "Please, specify only two ExamplesTable-s");
ExamplesTable leftTable = tables.get(0);
ExamplesTable rightTable = tables.get(1);
String leftTableJoinColumn = properties.getMandatoryNonBlankProperty("leftTableJoinColumn", String.class);
String rightTableJoinColumn = properties.getMandatoryNonBlankProperty("rightTableJoinColumn", String.class);
isTrue(leftTable.getHeaders().contains(leftTableJoinColumn),
"The left table doesn't contain the following column: %s", leftTableJoinColumn);
isTrue(rightTable.getHeaders().contains(rightTableJoinColumn),
"The right table doesn't contain the following column: %s", rightTableJoinColumn);
Set<String> repeatingKeys = leftTable.getHeaders().stream()
.filter(e -> !e.equals(leftTableJoinColumn) && rightTable.getHeaders().contains(e))
.collect(Collectors.toSet());
isTrue(repeatingKeys.isEmpty(), "Tables must contain different columns (except joint column),"
+ " but found the same columns: %s", repeatingKeys);

List<Map<String, String>> resultRows = join(leftTable, rightTable, leftTableJoinColumn, rightTableJoinColumn);
ExamplesTable resultTable = ExamplesTable.empty().withRows(resultRows);
return resultTable.isEmpty() ? getEmptyTableWithHeaders(tables, properties) : resultTable.asString();
}

protected abstract List<Map<String, String>> join(ExamplesTable leftTable, ExamplesTable rightTable,
String leftTableJoinColumn, String rightTableJoinColumn);

protected Map<String, String> joinMaps(Map<String, String> l, Map<String, String> r)
{
Map<String, String> jointRow = new HashMap<>();
jointRow.putAll(l);
jointRow.putAll(r);
return jointRow;
}

private static String getEmptyTableWithHeaders(List<ExamplesTable> tables, TableProperties tableProperties)
{
Set<String> headers = tables.stream().map(ExamplesTable::getHeaders)
.flatMap(List::stream).collect(Collectors.toSet());
return ExamplesTableProcessor.buildExamplesTable(headers, List.of(), tableProperties);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2019-2023 the original author or authors.
* Copyright 2019-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,73 +16,27 @@

package org.vividus.transformer;

import static org.apache.commons.lang3.Validate.isTrue;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import org.jbehave.core.model.ExamplesTable;
import org.jbehave.core.model.ExamplesTable.TableProperties;
import org.jbehave.core.model.TableParsers;
import org.vividus.util.ExamplesTableProcessor;

public class InnerJoinTableTransformer extends AbstractTableLoadingTransformer
public class InnerJoinTableTransformer extends AbstractJoinTableTransformer
{
public InnerJoinTableTransformer()
{
super(false);
}

@Override
public String transform(String tableAsString, TableParsers tableParsers, TableProperties properties)
{
List<ExamplesTable> tables = loadTables(tableAsString, properties);
isTrue(tables.size() == 2, "Please, specify only two ExamplesTable-s");
ExamplesTable leftTable = tables.get(0);
ExamplesTable rightTable = tables.get(1);
String leftTableJoinColumn = properties.getMandatoryNonBlankProperty("leftTableJoinColumn", String.class);
String rightTableJoinColumn = properties.getMandatoryNonBlankProperty("rightTableJoinColumn", String.class);
isTrue(leftTable.getHeaders().contains(leftTableJoinColumn),
"The left table doesn't contain the following column: %s", leftTableJoinColumn);
isTrue(rightTable.getHeaders().contains(rightTableJoinColumn),
"The right table doesn't contain the following column: %s", rightTableJoinColumn);
Set<String> repeatingKeys = leftTable.getHeaders().stream()
.filter(e -> !e.equals(leftTableJoinColumn) && rightTable.getHeaders().contains(e))
.collect(Collectors.toSet());
isTrue(repeatingKeys.isEmpty(), "Tables must contain different columns (except joint column),"
+ " but found the same columns: %s", repeatingKeys);

ExamplesTable resultTable = innerJoin(leftTable, rightTable, leftTableJoinColumn, rightTableJoinColumn);
return resultTable.isEmpty() ? getEmptyTableWithHeaders(tables, properties) : resultTable.asString();
}

private static ExamplesTable innerJoin(ExamplesTable leftTable, ExamplesTable rightTable,
protected List<Map<String, String>> join(ExamplesTable leftTable, ExamplesTable rightTable,
String leftTableJoinColumn, String rightTableJoinColumn)
{
List<Map<String, String>> leftRows = leftTable.getRows();
List<Map<String, String>> rightRows = rightTable.getRows();
Map<String, List<Map<String, String>>> rightByColumn = rightRows.stream()
.collect(Collectors.groupingBy(m -> m.get(rightTableJoinColumn)));
List<Map<String, String>> examplesTableRows = leftRows.stream()
.flatMap(leftRow -> Optional.ofNullable(rightByColumn.get(leftRow.get(leftTableJoinColumn)))
.stream().flatMap(value -> value.stream()
.map(rightRow -> {
Map<String, String> jointRow = new HashMap<>();
jointRow.putAll(leftRow);
jointRow.putAll(rightRow);
return jointRow;
}))).toList();
return ExamplesTable.empty().withRows(examplesTableRows);
}

private static String getEmptyTableWithHeaders(List<ExamplesTable> tables, TableProperties tableProperties)
{
Set<String> headers = tables.stream().map(ExamplesTable::getHeaders)
.flatMap(List::stream).collect(Collectors.toSet());
return ExamplesTableProcessor.buildExamplesTable(headers, List.of(), tableProperties);
return leftRows.stream()
.flatMap(leftRow -> Optional.ofNullable(rightByColumn.get(leftRow.get(leftTableJoinColumn)))
.stream()
.flatMap(value -> value.stream().map(rightRow -> joinMaps(leftRow, rightRow))))
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2019-2024 the original author or authors.
*
* 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
*
* https://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.vividus.transformer;

import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.commons.lang3.StringUtils;
import org.jbehave.core.model.ExamplesTable;

public final class LeftJoinTableTransformer extends AbstractJoinTableTransformer
{
@Override
protected List<Map<String, String>> join(ExamplesTable leftTable, ExamplesTable rightTable,
String leftTableJoinColumn, String rightTableJoinColumn)
{
Map<String, List<Map<String, String>>> rightByColumn = rightTable.getRows().stream()
.collect(Collectors.groupingBy(m -> m.get(rightTableJoinColumn)));

Map<String, String> emptyRightRow = rightTable.getHeaders().stream()
.filter(h -> !leftTable.getHeaders().contains(h))
.collect(Collectors.toMap(Function.identity(), k -> StringUtils.EMPTY));

return leftTable.getRows().stream()
.map(leftRow -> Optional.ofNullable(rightByColumn.get(leftRow.get(leftTableJoinColumn)))
.map(rows -> rows.stream().map(rightRow -> joinMaps(leftRow, rightRow)))
.orElseGet(() -> Stream.of(joinMaps(leftRow, emptyRightRow))))
.flatMap(Function.identity()).toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
<bean id="FILTERING" class="org.vividus.transformer.FilteringTableTransformer" />
<bean id="INDEXING" class="org.vividus.transformer.IndexingTableTransformer" />
<bean id="INNER_JOIN" class="org.vividus.transformer.InnerJoinTableTransformer" />
<bean id="LEFT_JOIN" class="org.vividus.transformer.LeftJoinTableTransformer" />

Check warning on line 81 in vividus/src/main/resources/org/vividus/spring-vividus.xml

View workflow job for this annotation

GitHub Actions / Qodana for JVM

Spring bean name violates conventions

'LEFT_JOIN' should start with lowercase letter
<bean id="ITERATING" class="org.vividus.transformer.IteratingTableTransformer" />
<bean id="JOINING" class="org.vividus.transformer.JoiningTableTransformer" />
<bean id="MERGING" class="org.vividus.transformer.MergingTableTransformer" />
Expand Down
Loading
Loading