Browse Source
Transform the tabular structure into a graph of RowDocument associated with nested documents and lists. Add container license acceptance for updated container images. See #1446 See #1450 See #1445 Original pull request: #1572pull/1619/head
7 changed files with 1062 additions and 33 deletions
@ -0,0 +1,203 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2023 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.springframework.data.jdbc.core.convert; |
||||||
|
|
||||||
|
import java.sql.ResultSet; |
||||||
|
import java.sql.ResultSetMetaData; |
||||||
|
import java.sql.SQLException; |
||||||
|
import java.util.Iterator; |
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
import org.springframework.dao.DataRetrievalFailureException; |
||||||
|
import org.springframework.data.jdbc.core.convert.RowDocumentExtractorSupport.AggregateContext; |
||||||
|
import org.springframework.data.jdbc.core.convert.RowDocumentExtractorSupport.RowDocumentSink; |
||||||
|
import org.springframework.data.jdbc.core.convert.RowDocumentExtractorSupport.TabularResultAdapter; |
||||||
|
import org.springframework.data.relational.core.mapping.AggregatePath; |
||||||
|
import org.springframework.data.relational.core.mapping.RelationalMappingContext; |
||||||
|
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; |
||||||
|
import org.springframework.data.relational.domain.RowDocument; |
||||||
|
import org.springframework.jdbc.support.JdbcUtils; |
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.util.LinkedCaseInsensitiveMap; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@link ResultSet}-driven extractor to extract {@link RowDocument documents}. |
||||||
|
* |
||||||
|
* @author Mark Paluch |
||||||
|
* @since 3.2 |
||||||
|
*/ |
||||||
|
class ResultSetRowDocumentExtractor { |
||||||
|
|
||||||
|
private final RelationalMappingContext context; |
||||||
|
private final PathToColumnMapping propertyToColumn; |
||||||
|
|
||||||
|
ResultSetRowDocumentExtractor(RelationalMappingContext context, PathToColumnMapping propertyToColumn) { |
||||||
|
this.context = context; |
||||||
|
this.propertyToColumn = propertyToColumn; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Adapter to extract values and column metadata from a {@link ResultSet}. |
||||||
|
*/ |
||||||
|
enum ResultSetAdapter implements TabularResultAdapter<ResultSet> { |
||||||
|
INSTANCE; |
||||||
|
|
||||||
|
@Override |
||||||
|
public Object getObject(ResultSet row, int index) { |
||||||
|
try { |
||||||
|
return JdbcUtils.getResultSetValue(row, index); |
||||||
|
} catch (SQLException e) { |
||||||
|
throw new DataRetrievalFailureException("Cannot retrieve column " + index + " from ResultSet", e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Map<String, Integer> getColumnMap(ResultSet result) { |
||||||
|
|
||||||
|
try { |
||||||
|
ResultSetMetaData metaData = result.getMetaData(); |
||||||
|
Map<String, Integer> columns = new LinkedCaseInsensitiveMap<>(metaData.getColumnCount()); |
||||||
|
|
||||||
|
for (int i = 0; i < metaData.getColumnCount(); i++) { |
||||||
|
columns.put(metaData.getColumnLabel(i + 1), i + 1); |
||||||
|
} |
||||||
|
return columns; |
||||||
|
} catch (SQLException e) { |
||||||
|
throw new DataRetrievalFailureException("Cannot retrieve ColumnMap from ResultSet", e); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Reads the next {@link RowDocument} from the {@link ResultSet}. The result set can be pristine (i.e. |
||||||
|
* {@link ResultSet#isBeforeFirst()}) or pointing already at a row. |
||||||
|
* |
||||||
|
* @param entity entity defining the document structure. |
||||||
|
* @param resultSet the result set to consume. |
||||||
|
* @return a {@link RowDocument}. |
||||||
|
* @throws SQLException if thrown by the JDBC API. |
||||||
|
* @throws IllegalStateException if the {@link ResultSet#isAfterLast() fully consumed}. |
||||||
|
*/ |
||||||
|
public RowDocument extractNextDocument(Class<?> entity, ResultSet resultSet) throws SQLException { |
||||||
|
return extractNextDocument(context.getRequiredPersistentEntity(entity), resultSet); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Reads the next {@link RowDocument} from the {@link ResultSet}. The result set can be pristine (i.e. |
||||||
|
* {@link ResultSet#isBeforeFirst()}) or pointing already at a row. |
||||||
|
* |
||||||
|
* @param entity entity defining the document structure. |
||||||
|
* @param resultSet the result set to consume. |
||||||
|
* @return a {@link RowDocument}. |
||||||
|
* @throws SQLException if thrown by the JDBC API. |
||||||
|
* @throws IllegalStateException if the {@link ResultSet#isAfterLast() fully consumed}. |
||||||
|
*/ |
||||||
|
public RowDocument extractNextDocument(RelationalPersistentEntity<?> entity, ResultSet resultSet) |
||||||
|
throws SQLException { |
||||||
|
|
||||||
|
Iterator<RowDocument> iterator = iterate(entity, resultSet); |
||||||
|
|
||||||
|
if (!iterator.hasNext()) { |
||||||
|
throw new IllegalStateException("ResultSet is fully consumed"); |
||||||
|
} |
||||||
|
|
||||||
|
return iterator.next(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Obtain a {@link Iterator} to retrieve {@link RowDocument documents} from a {@link ResultSet}. |
||||||
|
* |
||||||
|
* @param entity the entity to determine the document structure. |
||||||
|
* @param rs the input result set. |
||||||
|
* @return an iterator to consume the {@link ResultSet} as RowDocuments. |
||||||
|
* @throws SQLException if thrown by the JDBC API. |
||||||
|
*/ |
||||||
|
public Iterator<RowDocument> iterate(RelationalPersistentEntity<?> entity, ResultSet rs) throws SQLException { |
||||||
|
return new RowDocumentIterator(entity, rs); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Iterator implementation that advances through the {@link ResultSet} and feeds its input into a |
||||||
|
* {@link org.springframework.data.jdbc.core.convert.RowDocumentExtractorSupport.RowDocumentSink}. |
||||||
|
*/ |
||||||
|
private class RowDocumentIterator implements Iterator<RowDocument> { |
||||||
|
|
||||||
|
private final ResultSet resultSet; |
||||||
|
private final AggregatePath rootPath; |
||||||
|
private final RelationalPersistentEntity<?> rootEntity; |
||||||
|
private final Integer identifierIndex; |
||||||
|
private final AggregateContext<ResultSet> aggregateContext; |
||||||
|
|
||||||
|
private final boolean initiallyConsumed; |
||||||
|
private boolean hasNext; |
||||||
|
|
||||||
|
RowDocumentIterator(RelationalPersistentEntity<?> entity, ResultSet resultSet) throws SQLException { |
||||||
|
|
||||||
|
ResultSetAdapter adapter = ResultSetAdapter.INSTANCE; |
||||||
|
|
||||||
|
if (resultSet.isBeforeFirst()) { |
||||||
|
hasNext = resultSet.next(); |
||||||
|
} |
||||||
|
|
||||||
|
this.initiallyConsumed = resultSet.isAfterLast(); |
||||||
|
this.rootPath = context.getAggregatePath(entity); |
||||||
|
this.rootEntity = entity; |
||||||
|
|
||||||
|
String idColumn = propertyToColumn.column(rootPath.append(entity.getRequiredIdProperty())); |
||||||
|
Map<String, Integer> columns = adapter.getColumnMap(resultSet); |
||||||
|
this.aggregateContext = new AggregateContext<>(adapter, context, propertyToColumn, columns); |
||||||
|
|
||||||
|
this.resultSet = resultSet; |
||||||
|
this.identifierIndex = columns.get(idColumn); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean hasNext() { |
||||||
|
|
||||||
|
if (initiallyConsumed) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
return hasNext; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
@Nullable |
||||||
|
public RowDocument next() { |
||||||
|
|
||||||
|
RowDocumentSink<ResultSet> reader = new RowDocumentSink<>(aggregateContext, rootEntity, rootPath); |
||||||
|
Object key = ResultSetAdapter.INSTANCE.getObject(resultSet, identifierIndex); |
||||||
|
|
||||||
|
try { |
||||||
|
do { |
||||||
|
Object nextKey = ResultSetAdapter.INSTANCE.getObject(resultSet, identifierIndex); |
||||||
|
|
||||||
|
if (nextKey != null && !nextKey.equals(key)) { |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
reader.accept(resultSet); |
||||||
|
hasNext = resultSet.next(); |
||||||
|
} while (hasNext); |
||||||
|
} catch (SQLException e) { |
||||||
|
throw new DataRetrievalFailureException("Cannot advance ResultSet", e); |
||||||
|
} |
||||||
|
|
||||||
|
return reader.getResult(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,485 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2023 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.springframework.data.jdbc.core.convert; |
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.Comparator; |
||||||
|
import java.util.LinkedHashMap; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.TreeMap; |
||||||
|
|
||||||
|
import org.springframework.data.relational.core.mapping.AggregatePath; |
||||||
|
import org.springframework.data.relational.core.mapping.RelationalMappingContext; |
||||||
|
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; |
||||||
|
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; |
||||||
|
import org.springframework.data.relational.core.sql.SqlIdentifier; |
||||||
|
import org.springframework.data.relational.domain.RowDocument; |
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
|
||||||
|
/** |
||||||
|
* Support class for {@code ResultSet}-driven extractor implementations extracting {@link RowDocument documents} from |
||||||
|
* flexible input streams. |
||||||
|
* |
||||||
|
* @author Mark Paluch |
||||||
|
* @since 3.2 |
||||||
|
*/ |
||||||
|
abstract class RowDocumentExtractorSupport { |
||||||
|
|
||||||
|
/** |
||||||
|
* Result adapter to obtain values and column metadata. |
||||||
|
* |
||||||
|
* @param <RS> |
||||||
|
*/ |
||||||
|
interface TabularResultAdapter<RS> { |
||||||
|
|
||||||
|
/** |
||||||
|
* Read a value from the row input at {@code index}. |
||||||
|
* |
||||||
|
* @param row the row to read from. |
||||||
|
* @param index the column index. |
||||||
|
* @return the column value. Can be {@code null}. |
||||||
|
*/ |
||||||
|
@Nullable |
||||||
|
Object getObject(RS row, int index); |
||||||
|
|
||||||
|
/** |
||||||
|
* Retrieve a column name to column index map for access by column name. |
||||||
|
* |
||||||
|
* @param result the result set to read from. |
||||||
|
* @return column name to column index map. |
||||||
|
*/ |
||||||
|
Map<String, Integer> getColumnMap(RS result); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Reading context encapsulating value reading and column handling. |
||||||
|
* |
||||||
|
* @param <RS> |
||||||
|
*/ |
||||||
|
protected static class AggregateContext<RS> { |
||||||
|
|
||||||
|
private final TabularResultAdapter<RS> adapter; |
||||||
|
final RelationalMappingContext context; |
||||||
|
final PathToColumnMapping propertyToColumn; |
||||||
|
private final Map<String, Integer> columnMap; |
||||||
|
|
||||||
|
protected AggregateContext(TabularResultAdapter<RS> adapter, RelationalMappingContext context, |
||||||
|
PathToColumnMapping propertyToColumn, Map<String, Integer> columnMap) { |
||||||
|
this.adapter = adapter; |
||||||
|
this.context = context; |
||||||
|
this.propertyToColumn = propertyToColumn; |
||||||
|
this.columnMap = columnMap; |
||||||
|
} |
||||||
|
|
||||||
|
public RelationalPersistentEntity<?> getRequiredPersistentEntity(RelationalPersistentProperty property) { |
||||||
|
return context.getRequiredPersistentEntity(property); |
||||||
|
} |
||||||
|
|
||||||
|
public String getColumnName(AggregatePath path) { |
||||||
|
return propertyToColumn.column(path); |
||||||
|
} |
||||||
|
|
||||||
|
public String getKeyColumnName(AggregatePath path) { |
||||||
|
return propertyToColumn.keyColumn(path); |
||||||
|
} |
||||||
|
|
||||||
|
public boolean containsColumn(String columnName) { |
||||||
|
return columnMap.containsKey(columnName); |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
public Object getObject(RS row, String columnName) { |
||||||
|
return adapter.getObject(row, columnMap.get(columnName)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Collect the value for {@link AggregatePath} from the {@code row} and add it under {@link SqlIdentifier} to the |
||||||
|
* {@link RowDocument}. |
||||||
|
*/ |
||||||
|
void collectValue(RS row, AggregatePath source, RowDocument document, SqlIdentifier targetName) { |
||||||
|
|
||||||
|
String columnLabel = propertyToColumn.column(source); |
||||||
|
Integer index = columnMap.get(columnLabel); |
||||||
|
|
||||||
|
if (index == null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
Object resultSetValue = adapter.getObject(row, index); |
||||||
|
if (resultSetValue == null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
document.put(targetName.getReference(), resultSetValue); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Sink abstraction for tabular result sets that represent an aggregate including all of its nested entities. Reading |
||||||
|
* is driven by the results and readers receive a feed of rows to extract the data they are looking for. |
||||||
|
* <p> |
||||||
|
* Sinks aim to produce a {@link #getResult() result}. Based on the inputs, results may be {@link #hasResult() |
||||||
|
* present} or absent. |
||||||
|
*/ |
||||||
|
protected abstract static class TabularSink<RS> { |
||||||
|
|
||||||
|
/** |
||||||
|
* Accept a row of data and process their results to form potentially a {@link #getResult() result}. |
||||||
|
* |
||||||
|
* @param row the row to read from. |
||||||
|
*/ |
||||||
|
abstract void accept(RS row); |
||||||
|
|
||||||
|
/** |
||||||
|
* @return {@code true} if the sink has produced a result. |
||||||
|
*/ |
||||||
|
abstract boolean hasResult(); |
||||||
|
|
||||||
|
/** |
||||||
|
* Retrieve the sink result if present. |
||||||
|
* |
||||||
|
* @return the sink result. |
||||||
|
*/ |
||||||
|
@Nullable |
||||||
|
abstract Object getResult(); |
||||||
|
|
||||||
|
/** |
||||||
|
* Reset the sink to prepare for the next result. |
||||||
|
*/ |
||||||
|
abstract void reset(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Entity-driven sink to form a {@link RowDocument document} representing the underlying entities properties. |
||||||
|
* |
||||||
|
* @param <RS> |
||||||
|
*/ |
||||||
|
protected static class RowDocumentSink<RS> extends TabularSink<RS> { |
||||||
|
|
||||||
|
private final AggregateContext<RS> aggregateContext; |
||||||
|
private final RelationalPersistentEntity<?> entity; |
||||||
|
private final AggregatePath basePath; |
||||||
|
|
||||||
|
private RowDocument result; |
||||||
|
private final Map<RelationalPersistentProperty, TabularSink<RS>> readerState = new LinkedHashMap<>(); |
||||||
|
|
||||||
|
public RowDocumentSink(AggregateContext<RS> aggregateContext, RelationalPersistentEntity<?> entity, |
||||||
|
AggregatePath basePath) { |
||||||
|
this.aggregateContext = aggregateContext; |
||||||
|
this.entity = entity; |
||||||
|
this.basePath = basePath; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
void accept(RS row) { |
||||||
|
|
||||||
|
boolean first = result == null; |
||||||
|
|
||||||
|
if (first) { |
||||||
|
RowDocument document = new RowDocument(); |
||||||
|
readFirstRow(row, document); |
||||||
|
this.result = document; |
||||||
|
} |
||||||
|
|
||||||
|
for (TabularSink<RS> reader : readerState.values()) { |
||||||
|
reader.accept(row); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* First row contains the root aggregate and all headers for nested collections/maps/entities. |
||||||
|
*/ |
||||||
|
private void readFirstRow(RS row, RowDocument document) { |
||||||
|
|
||||||
|
for (RelationalPersistentProperty property : entity) { |
||||||
|
|
||||||
|
AggregatePath path = basePath.append(property); |
||||||
|
|
||||||
|
if (property.isQualified()) { |
||||||
|
readerState.put(property, new ContainerSink<>(aggregateContext, property, path)); |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
if (property.isEmbedded()) { |
||||||
|
collectEmbeddedValues(row, document, property, path); |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
if (property.isEntity()) { |
||||||
|
readerState.put(property, |
||||||
|
new RowDocumentSink<>(aggregateContext, aggregateContext.getRequiredPersistentEntity(property), path)); |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
aggregateContext.collectValue(row, path, document, property.getColumnName()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Read properties of embedded from the result set and store them under their column names |
||||||
|
*/ |
||||||
|
private void collectEmbeddedValues(RS row, RowDocument document, RelationalPersistentProperty property, |
||||||
|
AggregatePath path) { |
||||||
|
|
||||||
|
RelationalPersistentEntity<?> embeddedHolder = aggregateContext.getRequiredPersistentEntity(property); |
||||||
|
for (RelationalPersistentProperty embeddedProperty : embeddedHolder) { |
||||||
|
|
||||||
|
if (embeddedProperty.isQualified() || embeddedProperty.isCollectionLike() || embeddedProperty.isEntity()) { |
||||||
|
// hell, no!
|
||||||
|
throw new UnsupportedOperationException("Reading maps and collections into embeddable isn't supported yet"); |
||||||
|
} |
||||||
|
|
||||||
|
AggregatePath nested = path.append(embeddedProperty); |
||||||
|
aggregateContext.collectValue(row, nested, document, nested.getColumnInfo().name()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
boolean hasResult() { |
||||||
|
|
||||||
|
if (result == null) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
for (TabularSink<?> value : readerState.values()) { |
||||||
|
if (value.hasResult()) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return !result.isEmpty(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
RowDocument getResult() { |
||||||
|
|
||||||
|
readerState.forEach((property, reader) -> { |
||||||
|
|
||||||
|
if (reader.hasResult()) { |
||||||
|
result.put(property.getColumnName().getReference(), reader.getResult()); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
void reset() { |
||||||
|
result = null; |
||||||
|
readerState.clear(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Sink using a single column to retrieve values from. |
||||||
|
* |
||||||
|
* @param <RS> |
||||||
|
*/ |
||||||
|
private static class SingleColumnSink<RS> extends TabularSink<RS> { |
||||||
|
|
||||||
|
private final AggregateContext<RS> aggregateContext; |
||||||
|
private final String columnName; |
||||||
|
|
||||||
|
private @Nullable Object value; |
||||||
|
|
||||||
|
public SingleColumnSink(AggregateContext<RS> aggregateContext, AggregatePath path) { |
||||||
|
this.aggregateContext = aggregateContext; |
||||||
|
this.columnName = path.getColumnInfo().name().getReference(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
void accept(RS row) { |
||||||
|
|
||||||
|
if (aggregateContext.containsColumn(columnName)) { |
||||||
|
value = aggregateContext.getObject(row, columnName); |
||||||
|
} else { |
||||||
|
value = null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
boolean hasResult() { |
||||||
|
return value != null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
Object getResult() { |
||||||
|
return getValue(); |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
public Object getValue() { |
||||||
|
return value; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
void reset() { |
||||||
|
value = null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* A sink that aggregates multiple values in a {@link CollectionContainer container} such as List or Map. Inner values |
||||||
|
* are determined by the value type while the key type is expected to be a simple type such a string or a number. |
||||||
|
* |
||||||
|
* @param <RS> |
||||||
|
*/ |
||||||
|
private static class ContainerSink<RS> extends TabularSink<RS> { |
||||||
|
|
||||||
|
private final String keyColumn; |
||||||
|
private final AggregateContext<RS> aggregateContext; |
||||||
|
|
||||||
|
private Object key; |
||||||
|
private boolean hasResult = false; |
||||||
|
|
||||||
|
private final TabularSink<RS> componentReader; |
||||||
|
private final CollectionContainer container; |
||||||
|
|
||||||
|
public ContainerSink(AggregateContext<RS> aggregateContext, RelationalPersistentProperty property, |
||||||
|
AggregatePath path) { |
||||||
|
|
||||||
|
this.aggregateContext = aggregateContext; |
||||||
|
this.keyColumn = aggregateContext.getKeyColumnName(path); |
||||||
|
this.componentReader = property.isEntity() |
||||||
|
? new RowDocumentSink<>(aggregateContext, aggregateContext.getRequiredPersistentEntity(property), path) |
||||||
|
: new SingleColumnSink<>(aggregateContext, path); |
||||||
|
|
||||||
|
this.container = property.isMap() ? new MapContainer() : new ListContainer(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
void accept(RS row) { |
||||||
|
|
||||||
|
if (!aggregateContext.containsColumn(keyColumn)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
Object key = aggregateContext.getObject(row, keyColumn); |
||||||
|
if (key == null && !hasResult) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
boolean keyChange = key != null && !key.equals(this.key); |
||||||
|
|
||||||
|
if (!hasResult) { |
||||||
|
hasResult = true; |
||||||
|
} |
||||||
|
|
||||||
|
if (keyChange) { |
||||||
|
if (componentReader.hasResult()) { |
||||||
|
container.add(this.key, componentReader.getResult()); |
||||||
|
componentReader.reset(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (key != null) { |
||||||
|
this.key = key; |
||||||
|
} |
||||||
|
|
||||||
|
this.componentReader.accept(row); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean hasResult() { |
||||||
|
return hasResult; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Object getResult() { |
||||||
|
|
||||||
|
if (componentReader.hasResult()) { |
||||||
|
container.add(this.key, componentReader.getResult()); |
||||||
|
componentReader.reset(); |
||||||
|
} |
||||||
|
|
||||||
|
return container.get(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
void reset() { |
||||||
|
hasResult = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Base class defining method signatures to add values to a container that can hold multiple values, such as a List or |
||||||
|
* Map. |
||||||
|
*/ |
||||||
|
private abstract static class CollectionContainer { |
||||||
|
|
||||||
|
/** |
||||||
|
* Append the value. |
||||||
|
* |
||||||
|
* @param key the entry key/index. |
||||||
|
* @param value the entry value, can be {@literal null}. |
||||||
|
*/ |
||||||
|
abstract void add(Object key, @Nullable Object value); |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the container holding the values that were previously added. |
||||||
|
* |
||||||
|
* @return the container holding the values that were previously added. |
||||||
|
*/ |
||||||
|
abstract Object get(); |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: Are we 0 or 1 based?
|
||||||
|
private static class ListContainer extends CollectionContainer { |
||||||
|
|
||||||
|
private final Map<Number, Object> list = new TreeMap<>(Comparator.comparing(Number::longValue)); |
||||||
|
|
||||||
|
@Override |
||||||
|
public void add(Object key, @Nullable Object value) { |
||||||
|
list.put(((Number) key).intValue() - 1, value); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public List<Object> get() { |
||||||
|
|
||||||
|
List<Object> result = new ArrayList<>(list.size()); |
||||||
|
|
||||||
|
// TODO: How do we go about padding? Should we insert null values?
|
||||||
|
list.forEach((index, o) -> { |
||||||
|
|
||||||
|
while (result.size() < index.intValue()) { |
||||||
|
result.add(null); |
||||||
|
} |
||||||
|
|
||||||
|
result.add(o); |
||||||
|
}); |
||||||
|
|
||||||
|
return result; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static class MapContainer extends CollectionContainer { |
||||||
|
|
||||||
|
private final Map<Object, Object> map = new LinkedHashMap<>(); |
||||||
|
|
||||||
|
@Override |
||||||
|
public void add(Object key, @Nullable Object value) { |
||||||
|
map.put(key, value); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Map<Object, Object> get() { |
||||||
|
return new LinkedHashMap<>(map); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,255 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2023 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.springframework.data.relational.domain; |
||||||
|
|
||||||
|
import java.util.Collection; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.Set; |
||||||
|
import java.util.function.BiConsumer; |
||||||
|
import java.util.function.BiFunction; |
||||||
|
import java.util.function.Function; |
||||||
|
|
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.util.LinkedCaseInsensitiveMap; |
||||||
|
import org.springframework.util.ObjectUtils; |
||||||
|
|
||||||
|
/** |
||||||
|
* Represents a tabular structure as document to enable hierarchical traversal of SQL results. |
||||||
|
* |
||||||
|
* @author Mark Paluch |
||||||
|
* @since 3.2 |
||||||
|
*/ |
||||||
|
public class RowDocument implements Map<String, Object> { |
||||||
|
|
||||||
|
private final Map<String, Object> delegate; |
||||||
|
|
||||||
|
public RowDocument() { |
||||||
|
this.delegate = new LinkedCaseInsensitiveMap<>(); |
||||||
|
} |
||||||
|
|
||||||
|
public RowDocument(Map<String, Object> map) { |
||||||
|
this.delegate = new LinkedCaseInsensitiveMap<>(); |
||||||
|
this.delegate.putAll(delegate); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Retrieve the value at {@code key} as {@link List}. |
||||||
|
* |
||||||
|
* @param key |
||||||
|
* @return the value or {@literal null}. |
||||||
|
* @throws ClassCastException if {@code key} holds a value that is not a {@link List}. |
||||||
|
*/ |
||||||
|
@Nullable |
||||||
|
public List getList(String key) { |
||||||
|
|
||||||
|
Object item = get(key); |
||||||
|
if (item instanceof List<?> || item == null) { |
||||||
|
return (List) item; |
||||||
|
} |
||||||
|
|
||||||
|
throw new ClassCastException(String.format("Cannot cast element %s be cast to List", item)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Retrieve the value at {@code key} as {@link Map}. |
||||||
|
* |
||||||
|
* @param key |
||||||
|
* @return the value or {@literal null}. |
||||||
|
* @throws ClassCastException if {@code key} holds a value that is not a {@link Map}. |
||||||
|
*/ |
||||||
|
@Nullable |
||||||
|
public Map<String, Object> getMap(String key) { |
||||||
|
|
||||||
|
Object item = get(key); |
||||||
|
if (item instanceof Map || item == null) { |
||||||
|
return (Map<String, Object>) item; |
||||||
|
} |
||||||
|
|
||||||
|
throw new ClassCastException(String.format("Cannot cast element %s be cast to Map", item)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Retrieve the value at {@code key} as {@link RowDocument}. |
||||||
|
* |
||||||
|
* @param key |
||||||
|
* @return the value or {@literal null}. |
||||||
|
* @throws ClassCastException if {@code key} holds a value that is not a {@link RowDocument}. |
||||||
|
*/ |
||||||
|
public RowDocument getDocument(String key) { |
||||||
|
|
||||||
|
Object item = get(key); |
||||||
|
if (item instanceof RowDocument || item == null) { |
||||||
|
return (RowDocument) item; |
||||||
|
} |
||||||
|
|
||||||
|
throw new ClassCastException(String.format("Cannot cast element %s be cast to RowDocument", item)); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int size() { |
||||||
|
return delegate.size(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean isEmpty() { |
||||||
|
return delegate.isEmpty(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean containsKey(Object key) { |
||||||
|
return delegate.containsKey(key); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean containsValue(Object value) { |
||||||
|
return delegate.containsValue(value); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Object get(Object key) { |
||||||
|
return delegate.get(key); |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public Object put(String key, Object value) { |
||||||
|
return delegate.put(key, value); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Appends a new entry (or overwrites an existing value at {@code key}). |
||||||
|
* |
||||||
|
* @param key |
||||||
|
* @param value |
||||||
|
* @return |
||||||
|
*/ |
||||||
|
public RowDocument append(String key, Object value) { |
||||||
|
|
||||||
|
put(key, value); |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Object remove(Object key) { |
||||||
|
return delegate.remove(key); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void putAll(Map<? extends String, ?> m) { |
||||||
|
delegate.putAll(m); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void clear() { |
||||||
|
delegate.clear(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Set<String> keySet() { |
||||||
|
return delegate.keySet(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Collection<Object> values() { |
||||||
|
return delegate.values(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Set<Entry<String, Object>> entrySet() { |
||||||
|
return delegate.entrySet(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean equals(Object o) { |
||||||
|
if (this == o) |
||||||
|
return true; |
||||||
|
if (o == null || getClass() != o.getClass()) |
||||||
|
return false; |
||||||
|
|
||||||
|
RowDocument that = (RowDocument) o; |
||||||
|
|
||||||
|
return ObjectUtils.nullSafeEquals(delegate, that.delegate); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int hashCode() { |
||||||
|
return ObjectUtils.nullSafeHashCode(delegate); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Object getOrDefault(Object key, Object defaultValue) { |
||||||
|
return delegate.getOrDefault(key, defaultValue); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void forEach(BiConsumer<? super String, ? super Object> action) { |
||||||
|
delegate.forEach(action); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void replaceAll(BiFunction<? super String, ? super Object, ?> function) { |
||||||
|
delegate.replaceAll(function); |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public Object putIfAbsent(String key, Object value) { |
||||||
|
return delegate.putIfAbsent(key, value); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean remove(Object key, Object value) { |
||||||
|
return delegate.remove(key, value); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean replace(String key, Object oldValue, Object newValue) { |
||||||
|
return delegate.replace(key, oldValue, newValue); |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public Object replace(String key, Object value) { |
||||||
|
return delegate.replace(key, value); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Object computeIfAbsent(String key, Function<? super String, ?> mappingFunction) { |
||||||
|
return delegate.computeIfAbsent(key, mappingFunction); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Object computeIfPresent(String key, BiFunction<? super String, ? super Object, ?> remappingFunction) { |
||||||
|
return delegate.computeIfPresent(key, remappingFunction); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Object compute(String key, BiFunction<? super String, ? super Object, ?> remappingFunction) { |
||||||
|
return delegate.compute(key, remappingFunction); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Object merge(String key, Object value, BiFunction<? super Object, ? super Object, ?> remappingFunction) { |
||||||
|
return delegate.merge(key, value, remappingFunction); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String toString() { |
||||||
|
return getClass().getSimpleName() + delegate.toString(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
Loading…
Reference in new issue