Skip to content

Commit fc90315

Browse files
committed
object mapping using reflection API
1 parent a892a45 commit fc90315

20 files changed

+1030
-5
lines changed

clickhouse-client/src/main/java/com/clickhouse/client/ClickHouseResponse.java

+29-2
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,19 @@ public ClickHouseResponseSummary getSummary() {
5252

5353
@Override
5454
public ClickHouseInputStream getInputStream() {
55-
return null;
55+
return ClickHouseInputStream.empty();
5656
}
5757

5858
@Override
5959
public Iterable<ClickHouseRecord> records() {
6060
return Collections.emptyList();
6161
}
6262

63+
@Override
64+
public <T> Iterable<T> records(Class<T> objClass) {
65+
return Collections.emptyList();
66+
}
67+
6368
@Override
6469
public void close() {
6570
// do nothing
@@ -93,7 +98,7 @@ public boolean isClosed() {
9398
* also means additional work is required for deserialization, especially when
9499
* using a binary format.
95100
*
96-
* @return input stream for getting raw data returned from server
101+
* @return non-null input stream for getting raw data returned from server
97102
*/
98103
ClickHouseInputStream getInputStream();
99104

@@ -120,6 +125,18 @@ default ClickHouseRecord firstRecord() {
120125
*/
121126
Iterable<ClickHouseRecord> records();
122127

128+
/**
129+
* Returns an iterable collection of mapped objects which can be walked through
130+
* in a foreach loop. When {@code objClass} is null or {@link ClickHouseRecord},
131+
* it's same as calling {@link #records()}.
132+
*
133+
* @param <T> type of the mapped object
134+
* @param objClass non-null class of the mapped object
135+
* @return non-null iterable collection
136+
* @throws UncheckedIOException when failed to read data(e.g. deserialization)
137+
*/
138+
<T> Iterable<T> records(Class<T> objClass);
139+
123140
/**
124141
* Pipes the contents of this response into the given output stream. Keep in
125142
* mind that it's caller's responsibility to flush and close the output stream.
@@ -146,6 +163,16 @@ default Stream<ClickHouseRecord> stream() {
146163
Spliterator.IMMUTABLE | Spliterator.NONNULL | Spliterator.ORDERED), false);
147164
}
148165

166+
/**
167+
* Gets stream of mapped objects to process.
168+
*
169+
* @return stream of mapped objects
170+
*/
171+
default <T> Stream<T> stream(Class<T> objClass) {
172+
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(records(objClass).iterator(),
173+
Spliterator.IMMUTABLE | Spliterator.NONNULL | Spliterator.ORDERED), false);
174+
}
175+
149176
@Override
150177
void close();
151178

clickhouse-client/src/main/java/com/clickhouse/client/ClickHouseSimpleResponse.java

+11
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import com.clickhouse.data.ClickHouseRecordTransformer;
1212
import com.clickhouse.data.ClickHouseSimpleRecord;
1313
import com.clickhouse.data.ClickHouseValue;
14+
import com.clickhouse.data.mapper.IterableRecordWrapper;
1415

1516
/**
1617
* A simple response built on top of two lists: columns and records.
@@ -169,6 +170,16 @@ public Iterable<ClickHouseRecord> records() {
169170
return records;
170171
}
171172

173+
@SuppressWarnings("unchecked")
174+
@Override
175+
public <T> Iterable<T> records(Class<T> objClass) {
176+
if (objClass == null || objClass == ClickHouseRecord.class) {
177+
return (Iterable<T>) records();
178+
}
179+
180+
return () -> new IterableRecordWrapper<>(columns, records().iterator(), objClass);
181+
}
182+
172183
@Override
173184
public void close() {
174185
// nothing to close

clickhouse-client/src/main/java/com/clickhouse/client/ClickHouseStreamResponse.java

+9-1
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,15 @@ public Iterable<ClickHouseRecord> records() {
141141
throw new UnsupportedOperationException(
142142
"No data processor available for deserialization, please consider to use getInputStream instead");
143143
}
144-
145144
return processor.records();
146145
}
146+
147+
@Override
148+
public <T> Iterable<T> records(Class<T> objClass) {
149+
if (processor == null) {
150+
throw new UnsupportedOperationException(
151+
"No data processor available for deserialization, please consider to use getInputStream instead");
152+
}
153+
return processor.records(objClass);
154+
}
147155
}

clickhouse-data/pom.xml

+5
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@
8585
<artifactId>msgpack-core</artifactId>
8686
<optional>true</optional>
8787
</dependency>
88+
<dependency>
89+
<groupId>org.ow2.asm</groupId>
90+
<artifactId>asm</artifactId>
91+
<optional>true</optional>
92+
</dependency>
8893
<dependency>
8994
<groupId>org.slf4j</groupId>
9095
<artifactId>slf4j-api</artifactId>

clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseDataProcessor.java

+45-1
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ public ClickHouseValue next() {
155155
protected final ClickHouseInputStream input;
156156
protected final ClickHouseOutputStream output;
157157

158+
protected final Map<String, Serializable> extraProps;
159+
158160
protected DefaultSerDe serde;
159161
/**
160162
* Column index shared by {@link #read(ClickHouseValue)}, {@link #records()},
@@ -177,7 +179,7 @@ public ClickHouseValue next() {
177179
*/
178180
protected boolean hasMoreToRead() throws UncheckedIOException {
179181
try {
180-
if (input.available() <= 0) {
182+
if (input.available() < 1) {
181183
input.close();
182184
return false;
183185
}
@@ -389,6 +391,8 @@ protected ClickHouseDataProcessor(ClickHouseDataConfig config, ClickHouseInputSt
389391
this.input = input;
390392
this.output = output;
391393

394+
this.extraProps = new HashMap<>();
395+
392396
this.initialColumns = columns;
393397
this.initialSettings = settings;
394398
this.serde = null;
@@ -400,6 +404,27 @@ protected ClickHouseDataProcessor(ClickHouseDataConfig config, ClickHouseInputSt
400404
this.writePosition = 0;
401405
}
402406

407+
/**
408+
* Checks whether the processor contains extra property.
409+
*
410+
* @return true if the processor has extra property; false otherwise
411+
*/
412+
public boolean hasExtraProperties() {
413+
return extraProps.isEmpty();
414+
}
415+
416+
/**
417+
* Gets a typed extra property.
418+
*
419+
* @param <T> type of the property value
420+
* @param key key of the property
421+
* @param valueClass non-null Java class of the property value
422+
* @return typed extra property, could be null
423+
*/
424+
public <T extends Serializable> T getExtraProperty(String key, Class<T> valueClass) {
425+
return valueClass.cast(extraProps.get(key));
426+
}
427+
403428
public abstract ClickHouseDeserializer getDeserializer(ClickHouseDataConfig config, ClickHouseColumn column);
404429

405430
public final ClickHouseDeserializer[] getDeserializers(ClickHouseDataConfig config,
@@ -462,6 +487,25 @@ public final Iterable<ClickHouseRecord> records() {
462487
return () -> getInitializedSerDe().records;
463488
}
464489

490+
/**
491+
* Returns an iterable collection of mapped objects which can be walked through
492+
* in a foreach loop. When {@code objClass} is null or {@link ClickHouseRecord},
493+
* it's same as calling {@link #records()}.
494+
*
495+
* @param <T> type of the mapped object
496+
* @param objClass non-null class of the mapped object
497+
* @return non-null iterable collection
498+
* @throws UncheckedIOException when failed to read data(e.g. deserialization)
499+
*/
500+
@SuppressWarnings("unchecked")
501+
public <T> Iterable<T> records(Class<T> objClass) {
502+
if (objClass == null || objClass == ClickHouseRecord.class) {
503+
return (Iterable<T>) records();
504+
}
505+
506+
return () -> ClickHouseRecordMapper.wrap(getColumns(), getInitializedSerDe().records, objClass);
507+
}
508+
465509
/**
466510
* Returns an iterable collection of values which can be walked through in a
467511
* foreach-loop. In general, this is slower than {@link #records()}, because the

clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseRecord.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public int size() {
7272
@Override
7373
default Iterator<ClickHouseValue> iterator() {
7474
return new Iterator<ClickHouseValue>() {
75-
int index = 0;
75+
private int index = 0;
7676

7777
@Override
7878
public boolean hasNext() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package com.clickhouse.data;
2+
3+
import java.util.Iterator;
4+
import java.util.List;
5+
6+
import com.clickhouse.data.mapper.IterableRecordWrapper;
7+
import com.clickhouse.data.mapper.RecordMapperFactory;
8+
9+
/**
10+
* Functional interface for mapping {@link ClickHouseRecord} to customized
11+
* object.
12+
*/
13+
@FunctionalInterface
14+
public interface ClickHouseRecordMapper {
15+
/**
16+
* Wraps iterable records as mapped objects.
17+
*
18+
* @param <T> type of mapped object
19+
* @param columns non-null list of columns
20+
* @param records non-null iterable records
21+
* @param objClass non-null class of mapped object
22+
* @return non-null iterable objects
23+
*/
24+
static <T> Iterator<T> wrap(List<ClickHouseColumn> columns, Iterator<ClickHouseRecord> records, Class<T> objClass) {
25+
return new IterableRecordWrapper<>(columns, records, objClass);
26+
}
27+
28+
/**
29+
* Gets mapper to turn {@link ClickHouseRecord} into user-defined object.
30+
*
31+
* @param columns non-null list of columns
32+
* @param objClass non-null class of the user-defined object
33+
* @return non-null user-defined object
34+
*/
35+
static ClickHouseRecordMapper of(List<ClickHouseColumn> columns, Class<?> objClass) {
36+
if (columns == null || objClass == null) {
37+
throw new IllegalArgumentException("Non-null column list and object class are required");
38+
}
39+
40+
return RecordMapperFactory.of(columns, objClass);
41+
}
42+
43+
/**
44+
* Maps a record to a user-defined object. By default, it's same as
45+
* {@code mapTo(r, objClass, null)}.
46+
*
47+
* @param <T> type of the user-defined object
48+
* @param r non-null record
49+
* @param objClass non-null class of the user-defined object
50+
* @return non-null mapped object
51+
*/
52+
default <T> T mapTo(ClickHouseRecord r, Class<T> objClass) {
53+
return mapTo(r, objClass, null);
54+
}
55+
56+
/**
57+
* Maps a record to a user-defined object.
58+
*
59+
* @param <T> type of the user-defined object
60+
* @param r non-null record
61+
* @param objClass non-null class of the user-defined object
62+
* @param obj optional object instance to set values
63+
* @return non-null user-defined object, either new instance of
64+
* {@code objClass}, or same as the given {@code obj} when it's not null
65+
*/
66+
<T> T mapTo(ClickHouseRecord r, Class<T> objClass, T obj);
67+
}

clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseUtils.java

+25
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,31 @@ private static int readUnescapedJsonString(String json, StringBuilder builder, i
748748
return len;
749749
}
750750

751+
/**
752+
* Removes specific character from the given string.
753+
*
754+
* @param str string to remove character from
755+
* @param ch specific character to remove from the string
756+
* @return non-null string without the specific character
757+
*/
758+
public static String remove(String str, char ch) {
759+
if (str == null || str.isEmpty()) {
760+
return "";
761+
} else if (str.indexOf(ch) == -1) {
762+
return str;
763+
}
764+
765+
int len = str.length();
766+
StringBuilder builder = new StringBuilder(len);
767+
for (int i = 0; i < len; i++) {
768+
char c = str.charAt(i);
769+
if (c != ch) {
770+
builder.append(c);
771+
}
772+
}
773+
return builder.toString();
774+
}
775+
751776
/**
752777
* Simple and un-protected JSON parser.
753778
*

0 commit comments

Comments
 (0)