Skip to content

Commit 85673c6

Browse files
committed
Polishing.
Refactor enum to string codecs into StringDecoder and StringArrayDecoder as fallback codecs if no other codec could be found. Allow reuse of ArrayCodec. [#429][resolves #454] Signed-off-by: Mark Paluch <[email protected]>
1 parent 8b6e7c6 commit 85673c6

File tree

7 files changed

+448
-81
lines changed

7 files changed

+448
-81
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.r2dbc.postgresql.codec;
18+
19+
import io.netty.buffer.ByteBuf;
20+
import io.r2dbc.postgresql.util.Assert;
21+
22+
import java.lang.reflect.Array;
23+
import java.nio.charset.StandardCharsets;
24+
import java.util.ArrayList;
25+
import java.util.Arrays;
26+
import java.util.List;
27+
28+
import static io.r2dbc.postgresql.message.Format.FORMAT_BINARY;
29+
import static io.r2dbc.postgresql.message.Format.FORMAT_TEXT;
30+
31+
/**
32+
* Generic support class that provides a basis decoding codecs.
33+
*
34+
* @since 0.8.11
35+
*/
36+
class ArrayCodec {
37+
38+
static final byte COMMA = ',';
39+
40+
private static int getDimensions(List<?> list) {
41+
int dims = 1;
42+
43+
Object inner = list.get(0);
44+
45+
while (inner instanceof List) {
46+
inner = ((List<?>) inner).get(0);
47+
dims++;
48+
}
49+
50+
return dims;
51+
}
52+
53+
@SuppressWarnings("unchecked")
54+
static <T> T[] decodeBinary(ByteBuf buffer, int dataType, Decoder<T> decoder, Class<T> componentType, Class<?> returnType) {
55+
if (!buffer.isReadable()) {
56+
return (T[]) Array.newInstance(componentType, 0);
57+
}
58+
59+
int dimensions = buffer.readInt();
60+
if (dimensions == 0) {
61+
return (T[]) Array.newInstance(componentType, 0);
62+
}
63+
64+
if (returnType != Object.class) {
65+
Assert.requireArrayDimension(returnType, dimensions, "Dimensions mismatch: %s expected, but %s returned from DB");
66+
}
67+
68+
buffer.skipBytes(4); // flags: 0=no-nulls, 1=has-nulls
69+
buffer.skipBytes(4); // element oid
70+
71+
int[] dims = new int[dimensions];
72+
for (int d = 0; d < dimensions; ++d) {
73+
dims[d] = buffer.readInt(); // dimension size
74+
buffer.skipBytes(4); // lower bound ignored
75+
}
76+
77+
T[] array = (T[]) Array.newInstance(componentType, dims);
78+
79+
readArrayAsBinary(buffer, dataType, array, dims, decoder, componentType, 0);
80+
81+
return array;
82+
}
83+
84+
@SuppressWarnings("unchecked")
85+
static <T> T[] decodeText(ByteBuf buffer, int dataType, byte delimiter, Decoder<T> decoder, Class<T> componentType, Class<?> returnType) {
86+
List<T> elements = (List<T>) decodeText(buffer, delimiter, dataType, decoder, componentType);
87+
88+
if (elements.isEmpty()) {
89+
return (T[]) Array.newInstance(componentType, 0);
90+
}
91+
92+
int dimensions = getDimensions(elements);
93+
94+
if (returnType != Object.class) {
95+
Assert.requireArrayDimension(returnType, dimensions, "Dimensions mismatch: %s expected, but %s returned from DB");
96+
}
97+
98+
return toArray(elements, (Class<T>) createArrayType(componentType, dimensions).getComponentType());
99+
}
100+
101+
@SuppressWarnings("unchecked")
102+
private static <T> Class<T> createArrayType(Class<T> componentType, int dims) {
103+
int[] size = new int[dims];
104+
Arrays.fill(size, 1);
105+
return (Class<T>) Array.newInstance(componentType, size).getClass();
106+
}
107+
108+
@SuppressWarnings("unchecked")
109+
private static <T> T[] toArray(List<T> list, Class<T> returnType) {
110+
List<Object> result = new ArrayList<>(list.size());
111+
112+
for (Object e : list) {
113+
Object o = (e instanceof List ? toArray((List<Object>) e, (Class<Object>) returnType.getComponentType()) : e);
114+
result.add(o);
115+
}
116+
117+
return result.toArray((T[]) Array.newInstance(returnType, list.size()));
118+
}
119+
120+
private static <T> List<Object> decodeText(ByteBuf buf, byte delimiter, int dataType, Decoder<T> decoder, Class<T> componentType) {
121+
List<Object> arrayList = new ArrayList<>();
122+
123+
boolean insideString = false;
124+
boolean wasInsideString = false; // needed for checking if NULL
125+
// value occurred
126+
List<List<Object>> dims = new ArrayList<>(); // array dimension arrays
127+
List<Object> currentArray = arrayList; // currently processed array
128+
129+
// Starting with 8.0 non-standard (beginning index
130+
// isn't 1) bounds the dimensions are returned in the
131+
// data formatted like so "[0:3]={0,1,2,3,4}".
132+
// Older versions simply do not return the bounds.
133+
//
134+
// Right now we ignore these bounds, but we could
135+
// consider allowing these index values to be used
136+
// even though the JDBC spec says 1 is the first
137+
// index. I'm not sure what a client would like
138+
// to see, so we just retain the old behavior.
139+
140+
if (buf.isReadable() && buf.getByte(0) == '[') {
141+
while (buf.readByte() != '=') {
142+
//
143+
}
144+
}
145+
146+
int indentEscape = 0;
147+
int readFrom = 0;
148+
boolean requiresEscapeCharFiltering = false;
149+
while (buf.isReadable()) {
150+
151+
byte currentChar = buf.readByte();
152+
// escape character that we need to skip
153+
if (currentChar == '\\') {
154+
indentEscape++;
155+
buf.skipBytes(1);
156+
requiresEscapeCharFiltering = true;
157+
} else if (!insideString && currentChar == '{') {
158+
// subarray start
159+
if (dims.isEmpty()) {
160+
dims.add(arrayList);
161+
} else {
162+
List<Object> a = new ArrayList<>();
163+
List<Object> p = dims.get(dims.size() - 1);
164+
p.add(a);
165+
dims.add(a);
166+
}
167+
currentArray = dims.get(dims.size() - 1);
168+
169+
for (int t = indentEscape + 1; t < buf.writerIndex(); t++) {
170+
if (!Character.isWhitespace(buf.getByte(t)) && buf.getByte(t) != '{') {
171+
break;
172+
}
173+
}
174+
175+
readFrom = buf.readerIndex();
176+
} else if (currentChar == '"') {
177+
// quoted element
178+
insideString = !insideString;
179+
wasInsideString = true;
180+
} else if (!insideString && Character.isWhitespace(currentChar)) {
181+
// white space
182+
continue;
183+
} else if ((!insideString && (currentChar == delimiter || currentChar == '}'))
184+
|| indentEscape == buf.writerIndex() - 1) {
185+
// array end or element end
186+
// when character that is a part of array element
187+
int skipTrailingBytes = 0;
188+
if (currentChar != '}' && currentChar != delimiter && readFrom > 0) {
189+
skipTrailingBytes++;
190+
}
191+
192+
if (wasInsideString) {
193+
readFrom++;
194+
skipTrailingBytes++;
195+
}
196+
197+
ByteBuf slice = buf.slice(readFrom, (buf.readerIndex() - readFrom) - (skipTrailingBytes + /* skip current char as we've over-read */ 1));
198+
try {
199+
if (requiresEscapeCharFiltering) {
200+
ByteBuf filtered = slice.alloc().buffer(slice.readableBytes());
201+
while (slice.isReadable()) {
202+
byte ch = slice.readByte();
203+
if (ch == '\\') {
204+
ch = slice.readByte();
205+
}
206+
filtered.writeByte(ch);
207+
}
208+
slice = filtered;
209+
}
210+
211+
// add element to current array
212+
if (slice.isReadable() || wasInsideString) {
213+
if (!wasInsideString && slice.readableBytes() == 4 && slice.getByte(0) == 'N' && "NULL".equals(slice.toString(StandardCharsets.US_ASCII))) {
214+
currentArray.add(null);
215+
} else {
216+
currentArray.add(decoder.decode(slice, dataType, FORMAT_TEXT, componentType));
217+
}
218+
}
219+
} finally {
220+
221+
if (requiresEscapeCharFiltering) {
222+
slice.release();
223+
}
224+
}
225+
226+
wasInsideString = false;
227+
requiresEscapeCharFiltering = false;
228+
readFrom = buf.readerIndex();
229+
230+
// when end of an array
231+
if (currentChar == '}') {
232+
dims.remove(dims.size() - 1);
233+
234+
// when multi-dimension
235+
if (!dims.isEmpty()) {
236+
currentArray = dims.get(dims.size() - 1);
237+
}
238+
}
239+
}
240+
}
241+
242+
return arrayList;
243+
}
244+
245+
@SuppressWarnings("unchecked")
246+
247+
private static <T> void readArrayAsBinary(ByteBuf buffer, int dataType, Object[] array, int[] dims, Decoder<T> decoder, Class<T> componentType, int thisDimension) {
248+
if (thisDimension == dims.length - 1) {
249+
for (int i = 0; i < dims[thisDimension]; ++i) {
250+
int len = buffer.readInt();
251+
if (len == -1) {
252+
continue;
253+
}
254+
array[i] = decoder.decode(buffer.readSlice(len), dataType, FORMAT_BINARY, componentType);
255+
}
256+
} else {
257+
for (int i = 0; i < dims[thisDimension]; ++i) {
258+
readArrayAsBinary(buffer, dataType, (Object[]) array[i], dims, decoder, componentType, thisDimension + 1);
259+
}
260+
}
261+
}
262+
263+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.r2dbc.postgresql.codec;
18+
19+
import io.netty.buffer.ByteBuf;
20+
import io.r2dbc.postgresql.message.Format;
21+
import reactor.util.annotation.Nullable;
22+
23+
/**
24+
* Decoder for a specific {@code dataType} and {@link Class type}.
25+
*
26+
* @param <T> the type that is handled by this decoder.
27+
* @since 0.8.11
28+
*/
29+
interface Decoder<T> {
30+
31+
/**
32+
* Decode the {@link ByteBuf buffer} and return it as the requested {@link Class type}.
33+
*
34+
* @param buffer the data buffer
35+
* @param dataType the Postgres OID to encode
36+
* @param format the data type {@link Format}, text or binary
37+
* @param type the desired value type
38+
* @return the decoded value. Can be {@code null} if the value is {@code null}.
39+
*/
40+
@Nullable
41+
T decode(ByteBuf buffer, int dataType, Format format, Class<? extends T> type);
42+
43+
}

src/main/java/io/r2dbc/postgresql/codec/DefaultCodecs.java

+16-4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import io.netty.buffer.ByteBufAllocator;
2121
import io.r2dbc.postgresql.client.Parameter;
2222
import io.r2dbc.postgresql.message.Format;
23+
import io.r2dbc.postgresql.type.PostgresqlObjectId;
2324
import io.r2dbc.postgresql.util.Assert;
2425
import reactor.util.annotation.Nullable;
2526

@@ -164,6 +165,7 @@ public void addLast(Codec<?> codec) {
164165

165166
@Override
166167
@Nullable
168+
@SuppressWarnings("unchecked")
167169
public <T> T decode(@Nullable ByteBuf buffer, int dataType, Format format, Class<? extends T> type) {
168170
Assert.requireNonNull(format, "format must not be null");
169171
Assert.requireNonNull(type, "type must not be null");
@@ -175,14 +177,24 @@ public <T> T decode(@Nullable ByteBuf buffer, int dataType, Format format, Class
175177
Codec<T> codec = this.codecLookup.findDecodeCodec(dataType, format, type);
176178
if (codec != null) {
177179
return codec.decode(buffer, dataType, format, type);
178-
} else if (String.class == type) {
180+
}
181+
182+
if (String.class == type) {
179183
int varcharType = PostgresqlObjectId.VARCHAR.getObjectId();
180-
codec = this.codecLookup.findDecodeCodec(varcharType, format, type);
181-
if (codec != null) {
182-
return codec.decode(buffer, varcharType, format, type);
184+
Codec<T> varcharFallback = this.codecLookup.findDecodeCodec(varcharType, format, type);
185+
if (varcharFallback != null) {
186+
return varcharFallback.decode(buffer, varcharType, format, type);
183187
}
184188
}
185189

190+
if (StringCodec.STRING_DECODER.canDecode(dataType, format, type)) {
191+
return type.cast(StringCodec.STRING_DECODER.decode(buffer, dataType, format, (Class<String>) type));
192+
}
193+
194+
if (StringCodec.STRING_ARRAY_DECODER.canDecode(dataType, format, type)) {
195+
return type.cast(StringCodec.STRING_ARRAY_DECODER.decode(buffer, dataType, format, (Class<String[]>) type));
196+
}
197+
186198
throw new IllegalArgumentException(String.format("Cannot decode value of type %s with OID %d", type.getName(), dataType));
187199
}
188200

0 commit comments

Comments
 (0)