Skip to content

Commit 1e5937f

Browse files
committed
Extend DefaultUdtValue to override toString() method (#76)
This allows a human-readable string representation of UDT values in tools like DBeaver for example
1 parent 0f55b0f commit 1e5937f

File tree

3 files changed

+189
-10
lines changed

3 files changed

+189
-10
lines changed

Diff for: CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
1616
[#76](https://github.com/ing-bank/cassandra-jdbc-wrapper/issues/76)).
1717
- Handle lists of User-Defined Types (UDT) properly when using `CassandraResultSet.getObject(String|int)` methods (see
1818
issue [#76](https://github.com/ing-bank/cassandra-jdbc-wrapper/issues/76)).
19+
- Use an extended implementation of `UdtValue`, including a string representation of the value, for UDT values returned
20+
by `CassandraResultSet.getObject(String|int)` methods (see issue
21+
[#76](https://github.com/ing-bank/cassandra-jdbc-wrapper/issues/76)).
1922

2023
## [4.14.0] - 2024-12-24
2124
### Added

Diff for: src/main/java/com/ing/data/cassandra/jdbc/CassandraResultSet.java

+30-10
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import com.ing.data.cassandra.jdbc.types.DataTypeEnum;
3636
import com.ing.data.cassandra.jdbc.types.TypesMap;
3737
import com.ing.data.cassandra.jdbc.utils.ArrayImpl;
38+
import com.ing.data.cassandra.jdbc.utils.UdtUtil.WithFormattedContentsDefaultUdtValue;
3839
import org.apache.commons.collections4.IteratorUtils;
3940
import org.apache.commons.io.IOUtils;
4041
import org.apache.commons.lang3.StringUtils;
@@ -109,6 +110,8 @@
109110
import static com.ing.data.cassandra.jdbc.utils.ErrorConstants.VECTOR_ELEMENTS_NOT_NUMBERS;
110111
import static com.ing.data.cassandra.jdbc.utils.ErrorConstants.WAS_CLOSED_RS;
111112
import static com.ing.data.cassandra.jdbc.utils.JsonUtil.getObjectMapper;
113+
import static com.ing.data.cassandra.jdbc.utils.UdtUtil.udtValueUsingFormattedContents;
114+
import static com.ing.data.cassandra.jdbc.utils.UdtUtil.udtValuesUsingFormattedContents;
112115

113116
/**
114117
* Cassandra result set: implementation class for {@link java.sql.ResultSet}.
@@ -166,6 +169,11 @@
166169
* and {@link java.sql.ResultSet#getArray(String)}.
167170
* </p>
168171
*
172+
* @implNote User-defined types (UDT) values, including such values in lists, sets and maps, returned by the methods
173+
* {@link #getObject(int)} and {@link #getObject(String)} are instances of {@link WithFormattedContentsDefaultUdtValue}.
174+
* Be careful when using the method {@link WithFormattedContentsDefaultUdtValue#toString()} on these values to avoid
175+
* data leaks (e.g. in application logs). Some borderline cases, especially complex types involving nested collections
176+
* and nested UDT values may be not working correctly.
169177
* @see ResultSet
170178
*/
171179
public class CassandraResultSet extends AbstractResultSet
@@ -913,8 +921,8 @@ public Object getObject(final int columnIndex) throws SQLException {
913921
final Set<?> resultSet;
914922

915923
if (elementsType instanceof UserDefinedType) {
916-
resultSet = this.currentRow.getSet(columnIndex - 1,
917-
TypesMap.getTypeForComparator(DataTypeEnum.UDT.asLowercaseCql()).getType());
924+
resultSet = udtValuesUsingFormattedContents(this.currentRow.getSet(columnIndex - 1,
925+
TypesMap.getTypeForComparator(DataTypeEnum.UDT.asLowercaseCql()).getType()));
918926
} else if (elementsType instanceof TupleType) {
919927
resultSet = this.currentRow.getSet(columnIndex - 1,
920928
TypesMap.getTypeForComparator(DataTypeEnum.TUPLE.asLowercaseCql()).getType());
@@ -935,8 +943,8 @@ public Object getObject(final int columnIndex) throws SQLException {
935943
final List<?> resultList;
936944

937945
if (elementsType instanceof UserDefinedType) {
938-
resultList = this.currentRow.getList(columnIndex - 1,
939-
TypesMap.getTypeForComparator(DataTypeEnum.UDT.asLowercaseCql()).getType());
946+
resultList = udtValuesUsingFormattedContents(this.currentRow.getList(columnIndex - 1,
947+
TypesMap.getTypeForComparator(DataTypeEnum.UDT.asLowercaseCql()).getType()));
940948
} else if (elementsType instanceof TupleType) {
941949
resultList = this.currentRow.getList(columnIndex - 1,
942950
TypesMap.getTypeForComparator(DataTypeEnum.TUPLE.asLowercaseCql()).getType());
@@ -960,17 +968,20 @@ public Object getObject(final int columnIndex) throws SQLException {
960968
final MapType mapType = (MapType) cqlDataType;
961969
final DataType keyType = mapType.getKeyType();
962970
final DataType valueType = mapType.getValueType();
971+
boolean containsUdtValues = false;
963972

964973
Class<?> keyClass = TypesMap.getTypeForComparator(keyType.asCql(false, false)).getType();
965974
if (keyType instanceof UserDefinedType) {
966975
keyClass = TypesMap.getTypeForComparator(DataTypeEnum.UDT.asLowercaseCql()).getType();
976+
containsUdtValues = true;
967977
} else if (keyType instanceof TupleType) {
968978
keyClass = TypesMap.getTypeForComparator(DataTypeEnum.TUPLE.asLowercaseCql()).getType();
969979
}
970980

971981
Class<?> valueClass = TypesMap.getTypeForComparator(valueType.asCql(false, false)).getType();
972982
if (valueType instanceof UserDefinedType) {
973983
valueClass = TypesMap.getTypeForComparator(DataTypeEnum.UDT.asLowercaseCql()).getType();
984+
containsUdtValues = true;
974985
} else if (valueType instanceof TupleType) {
975986
valueClass = TypesMap.getTypeForComparator(DataTypeEnum.TUPLE.asLowercaseCql()).getType();
976987
}
@@ -979,6 +990,9 @@ public Object getObject(final int columnIndex) throws SQLException {
979990
if (resultMap == null) {
980991
return null;
981992
}
993+
if (containsUdtValues) {
994+
return udtValuesUsingFormattedContents(resultMap);
995+
}
982996
return new HashMap<>(resultMap);
983997
}
984998
} else {
@@ -1023,7 +1037,7 @@ public Object getObject(final int columnIndex) throws SQLException {
10231037
case TIMEUUID:
10241038
return this.currentRow.getUuid(columnIndex - 1);
10251039
case UDT:
1026-
return this.currentRow.getUdtValue(columnIndex - 1);
1040+
return udtValueUsingFormattedContents(this.currentRow.getUdtValue(columnIndex - 1));
10271041
case TUPLE:
10281042
return this.currentRow.getTupleValue(columnIndex - 1);
10291043
}
@@ -1051,8 +1065,8 @@ public Object getObject(final String columnLabel) throws SQLException {
10511065
final Set<?> resultSet;
10521066

10531067
if (elementsType instanceof UserDefinedType) {
1054-
resultSet = this.currentRow.getSet(columnLabel,
1055-
TypesMap.getTypeForComparator(DataTypeEnum.UDT.asLowercaseCql()).getType());
1068+
resultSet = udtValuesUsingFormattedContents(this.currentRow.getSet(columnLabel,
1069+
TypesMap.getTypeForComparator(DataTypeEnum.UDT.asLowercaseCql()).getType()));
10561070
} else if (elementsType instanceof TupleType) {
10571071
resultSet = this.currentRow.getSet(columnLabel,
10581072
TypesMap.getTypeForComparator(DataTypeEnum.TUPLE.asLowercaseCql()).getType());
@@ -1073,8 +1087,8 @@ public Object getObject(final String columnLabel) throws SQLException {
10731087
final List<?> resultList;
10741088

10751089
if (elementsType instanceof UserDefinedType) {
1076-
resultList = this.currentRow.getList(columnLabel,
1077-
TypesMap.getTypeForComparator(DataTypeEnum.UDT.asLowercaseCql()).getType());
1090+
resultList = udtValuesUsingFormattedContents(this.currentRow.getList(columnLabel,
1091+
TypesMap.getTypeForComparator(DataTypeEnum.UDT.asLowercaseCql()).getType()));
10781092
} else if (elementsType instanceof TupleType) {
10791093
resultList = this.currentRow.getList(columnLabel,
10801094
TypesMap.getTypeForComparator(DataTypeEnum.TUPLE.asLowercaseCql()).getType());
@@ -1098,17 +1112,20 @@ public Object getObject(final String columnLabel) throws SQLException {
10981112
final MapType mapType = (MapType) cqlDataType;
10991113
final DataType keyType = mapType.getKeyType();
11001114
final DataType valueType = mapType.getValueType();
1115+
boolean containsUdtValues = false;
11011116

11021117
Class<?> keyClass = TypesMap.getTypeForComparator(keyType.asCql(false, false)).getType();
11031118
if (keyType instanceof UserDefinedType) {
11041119
keyClass = TypesMap.getTypeForComparator(DataTypeEnum.UDT.asLowercaseCql()).getType();
1120+
containsUdtValues = true;
11051121
} else if (keyType instanceof TupleType) {
11061122
keyClass = TypesMap.getTypeForComparator(DataTypeEnum.TUPLE.asLowercaseCql()).getType();
11071123
}
11081124

11091125
Class<?> valueClass = TypesMap.getTypeForComparator(valueType.asCql(false, false)).getType();
11101126
if (valueType instanceof UserDefinedType) {
11111127
valueClass = TypesMap.getTypeForComparator(DataTypeEnum.UDT.asLowercaseCql()).getType();
1128+
containsUdtValues = true;
11121129
} else if (valueType instanceof TupleType) {
11131130
valueClass = TypesMap.getTypeForComparator(DataTypeEnum.TUPLE.asLowercaseCql()).getType();
11141131
}
@@ -1117,6 +1134,9 @@ public Object getObject(final String columnLabel) throws SQLException {
11171134
if (resultMap == null) {
11181135
return null;
11191136
}
1137+
if (containsUdtValues) {
1138+
return udtValuesUsingFormattedContents(resultMap);
1139+
}
11201140
return new HashMap<>(resultMap);
11211141
}
11221142
} else {
@@ -1161,7 +1181,7 @@ public Object getObject(final String columnLabel) throws SQLException {
11611181
case TIMEUUID:
11621182
return this.currentRow.getUuid(columnLabel);
11631183
case UDT:
1164-
return this.currentRow.getUdtValue(columnLabel);
1184+
return udtValueUsingFormattedContents(this.currentRow.getUdtValue(columnLabel));
11651185
case TUPLE:
11661186
return this.currentRow.getTupleValue(columnLabel);
11671187
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
package com.ing.data.cassandra.jdbc.utils;
17+
18+
import com.datastax.oss.driver.api.core.data.UdtValue;
19+
import com.datastax.oss.driver.api.core.type.DataType;
20+
import com.datastax.oss.driver.api.core.type.reflect.GenericType;
21+
import com.datastax.oss.driver.internal.core.data.DefaultUdtValue;
22+
23+
import java.util.List;
24+
import java.util.Map;
25+
import java.util.Set;
26+
import java.util.stream.Collectors;
27+
28+
/**
29+
* Utility methods used for User-defined types (UDTs) handling.
30+
*/
31+
public final class UdtUtil {
32+
33+
private UdtUtil() {
34+
// Private constructor to hide the public one.
35+
}
36+
37+
/**
38+
* Returns a list of UDT values using an implementation of {@link UdtValue} using the result of the method
39+
* {@link UdtValue#getFormattedContents()} as string representation of the UDT object.
40+
*
41+
* @param list The original list of UDT values.
42+
* @return An instance of {@link List} using {@link WithFormattedContentsDefaultUdtValue} for the items of the given
43+
* list, or {@code null} if the original list was {@code null}.
44+
*/
45+
public static List<UdtValue> udtValuesUsingFormattedContents(final List<?> list) {
46+
if (list != null) {
47+
return list.stream()
48+
.map(item -> udtValueUsingFormattedContents((UdtValue) item))
49+
.collect(Collectors.toList());
50+
}
51+
return null;
52+
}
53+
54+
/**
55+
* Returns a set of UDT values using an implementation of {@link UdtValue} using the result of the method
56+
* {@link UdtValue#getFormattedContents()} as string representation of the UDT object.
57+
*
58+
* @param set The original set of UDT values.
59+
* @return An instance of {@link Set} using {@link WithFormattedContentsDefaultUdtValue} for the items of the given
60+
* set, or {@code null} if the original set was {@code null}.
61+
*/
62+
public static Set<UdtValue> udtValuesUsingFormattedContents(final Set<?> set) {
63+
if (set != null) {
64+
return set.stream()
65+
.map(item -> udtValueUsingFormattedContents((UdtValue) item))
66+
.collect(Collectors.toSet());
67+
}
68+
return null;
69+
}
70+
71+
/**
72+
* Returns a map where the keys and/or values of type {@link UdtValue} are replaced by an implementation of
73+
* {@link UdtValue} using the result of the method {@link UdtValue#getFormattedContents()} as string representation
74+
* of the UDT object.
75+
*
76+
* @param map The original map containing some UDT values as keys and/or values.
77+
* @return An instance of {@link Map} using {@link WithFormattedContentsDefaultUdtValue} for the keys and/or values
78+
* of the given map which were UDT values, or {@code null} if the original map was {@code null}.
79+
*/
80+
public static Map<?, ?> udtValuesUsingFormattedContents(final Map<?, ?> map) {
81+
if (map != null) {
82+
return map.entrySet().stream()
83+
.collect(Collectors.toMap(
84+
entry -> {
85+
final Object key = entry.getKey();
86+
if (key.getClass().isAssignableFrom(UdtValue.class)) {
87+
return udtValueUsingFormattedContents((UdtValue) key);
88+
}
89+
return key;
90+
},
91+
entry -> {
92+
final Object value = entry.getValue();
93+
if (value.getClass().isAssignableFrom(UdtValue.class)) {
94+
return udtValueUsingFormattedContents((UdtValue) value);
95+
}
96+
return value;
97+
})
98+
);
99+
}
100+
return null;
101+
}
102+
103+
/**
104+
* Returns an implementation of {@link UdtValue} using the result of the method
105+
* {@link UdtValue#getFormattedContents()} as string representation of the UDT object.
106+
*
107+
* @param udtValue The original UDT value.
108+
* @return An instance of {@link WithFormattedContentsDefaultUdtValue} for the given UDT value or {@code null} if
109+
* the original value was {@code null}.
110+
*/
111+
public static UdtValue udtValueUsingFormattedContents(final UdtValue udtValue) {
112+
if (udtValue == null) {
113+
return null;
114+
}
115+
return new WithFormattedContentsDefaultUdtValue(udtValue);
116+
}
117+
118+
/**
119+
* Extended implementation of {@link DefaultUdtValue} overriding {@link DefaultUdtValue#toString()} method to
120+
* include the representation of the UDT contents.
121+
* <p>
122+
* Be careful, using this implementation may result in a data leak (e.g. in application logs) if the method
123+
* {@link #toString()} is called carelessly.
124+
* </p>
125+
*/
126+
public static final class WithFormattedContentsDefaultUdtValue extends DefaultUdtValue {
127+
/**
128+
* Instantiates a {@code WithFormattedContentsDefaultUdtValue} based on an original {@link UdtValue} instance.
129+
*
130+
* @param original The original UDT value.
131+
*/
132+
@SuppressWarnings("ResultOfMethodCallIgnored")
133+
WithFormattedContentsDefaultUdtValue(final UdtValue original) {
134+
super(original.getType());
135+
// Copy original values into this new instance.
136+
for (int i = 0; i < original.size(); i++) {
137+
if (!original.isNull(i)) {
138+
final DataType fieldDataType = original.getType().getFieldTypes().get(i);
139+
final GenericType<Object> fieldGenericType = codecRegistry().codecFor(fieldDataType).getJavaType();
140+
this.set(i, original.get(i, fieldGenericType), fieldGenericType);
141+
}
142+
}
143+
}
144+
145+
/**
146+
* Gets the string representation of the UDT contents for this object.
147+
*
148+
* @return The string representation of the contents of this UDT value.
149+
* @see #getFormattedContents()
150+
*/
151+
@Override
152+
public String toString() {
153+
return this.getFormattedContents();
154+
}
155+
}
156+
}

0 commit comments

Comments
 (0)