Skip to content

Commit 360e974

Browse files
committed
Introduce replaceCloseQuote to support " type replacements
1 parent 810eb4d commit 360e974

File tree

6 files changed

+98
-9
lines changed

6 files changed

+98
-9
lines changed

query-engine/docs/query-engine-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ services:
5050
- 16686:16686
5151

5252
query-engine:
53-
image: ghcr.io/yaytay/query-engine-design-mode:0.0.70-3-main
53+
image: ghcr.io/yaytay/query-engine-design-mode:0.0.71-main
5454
ports:
5555
- 2000:8080
5656
volumes:

query-engine/src/main/java/uk/co/spudsoft/query/defn/FormatDelimited.java

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public class FormatDelimited implements Format {
5151
private final String openQuote;
5252
private final String closeQuote;
5353
private final String escapeCloseQuote;
54+
private final String replaceCloseQuote;
5455
private final String newline;
5556

5657
@Override
@@ -275,10 +276,13 @@ public String getCloseQuote() {
275276

276277
/**
277278
* Value with which to prefix any occurrence of the {@link #closeQuote} string in an output string value.
279+
* Do not set both this and replaceCloseQuote, this value will take preference.
278280
* @return the value with which to prefix any occurrence of the {@link #closeQuote} string in an output string value.
279281
*/
280282
@Schema(description = """
281283
If a string value contains the close quote string it will be prefixed by this string.
284+
<P>
285+
Do not set both this and replaceCloseQuote, this value will take preference.
282286
"""
283287
, defaultValue = "\""
284288
, requiredMode = Schema.RequiredMode.NOT_REQUIRED
@@ -287,6 +291,24 @@ public String getCloseQuote() {
287291
public String getEscapeCloseQuote() {
288292
return escapeCloseQuote;
289293
}
294+
295+
/**
296+
* Value with which to replace any occurrence of the {@link #closeQuote} string in an output string value.
297+
* Do not set both this and escapeCloseQuote, the value of escapeCloseQuote will take preference.
298+
* @return the value with which to replace any occurrence of the {@link #closeQuote} string in an output string value.
299+
*/
300+
@Schema(description = """
301+
If a string value contains the close quote string it will be replaced by this string.
302+
<P>
303+
Do not set both this and escapeCloseQuote, the value of escapeCloseQuote will take preference.
304+
"""
305+
, defaultValue = "\""
306+
, requiredMode = Schema.RequiredMode.NOT_REQUIRED
307+
, maxLength = 10
308+
)
309+
public String getReplaceCloseQuote() {
310+
return replaceCloseQuote;
311+
}
290312

291313
/**
292314
* Value with which to suffix each row in the output.
@@ -321,7 +343,8 @@ public static class Builder {
321343
private String delimiter = ",";
322344
private String openQuote = "\"";
323345
private String closeQuote = "\"";
324-
private String escapeCloseQuote = "\"";
346+
private String escapeCloseQuote = "";
347+
private String replaceCloseQuote = "";
325348
private String newline = "\r\n";
326349

327350
private Builder() {
@@ -460,6 +483,16 @@ public Builder escapeCloseQuote(final String value) {
460483
return this;
461484
}
462485

486+
/**
487+
* Set the {@link FormatDelimited#replaceCloseQuote} value in the builder.
488+
* @param value The value for the {@link FormatDelimited#replaceCloseQuote}.
489+
* @return this, so that this builder may be used in a fluent manner.
490+
*/
491+
public Builder replaceCloseQuote(final String value) {
492+
this.replaceCloseQuote = value;
493+
return this;
494+
}
495+
463496
/**
464497
* Set the {@link FormatDelimited#newline} value in the builder.
465498
* @param value The value for the {@link FormatDelimited#newline}.
@@ -475,7 +508,7 @@ public Builder newline(final String value) {
475508
* @return a new instance of the FormatDelimited class.
476509
*/
477510
public FormatDelimited build() {
478-
return new FormatDelimited(type, name, description, extension, filename, mediaType, hidden, bom, headerRow, delimiter, openQuote, closeQuote, escapeCloseQuote, newline);
511+
return new FormatDelimited(type, name, description, extension, filename, mediaType, hidden, bom, headerRow, delimiter, openQuote, closeQuote, escapeCloseQuote, replaceCloseQuote, newline);
479512
}
480513
}
481514

@@ -500,6 +533,7 @@ private FormatDelimited(final FormatType type
500533
, final String openQuote
501534
, final String closeQuote
502535
, final String escapeCloseQuote
536+
, final String replaceCloseQuote
503537
, final String newline
504538
) {
505539
validateType(FormatType.Delimited, type);
@@ -516,6 +550,7 @@ private FormatDelimited(final FormatType type
516550
this.openQuote = openQuote;
517551
this.closeQuote = closeQuote;
518552
this.escapeCloseQuote = escapeCloseQuote;
553+
this.replaceCloseQuote = replaceCloseQuote;
519554
this.newline = newline;
520555
}
521556

query-engine/src/main/java/uk/co/spudsoft/query/exec/fmts/text/FormatDelimitedInstance.java

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import io.vertx.core.buffer.Buffer;
2525
import io.vertx.core.streams.WriteStream;
2626
import java.util.concurrent.atomic.AtomicBoolean;
27+
import java.util.regex.Matcher;
2728
import java.util.regex.Pattern;
2829
import org.slf4j.Logger;
2930
import org.slf4j.LoggerFactory;
@@ -147,10 +148,8 @@ private Future<Void> outputRow(DataRow row) {
147148
case String:
148149
default:
149150
outputRow.append(defn.getOpenQuote());
150-
String string = v.toString();
151-
if (!Strings.isNullOrEmpty(defn.getCloseQuote()) && !Strings.isNullOrEmpty(defn.getEscapeCloseQuote())) {
152-
string = string.replaceAll(Pattern.quote(defn.getCloseQuote()), defn.getEscapeCloseQuote() + defn.getCloseQuote());
153-
}
151+
String string = v.toString();
152+
string = encodeCloseQuote(defn, string);
154153
outputRow.append(string);
155154
outputRow.append(defn.getCloseQuote());
156155
break;
@@ -164,6 +163,22 @@ private Future<Void> outputRow(DataRow row) {
164163
return outputStream.write(Buffer.buffer(outputRow.toString()));
165164
}
166165

166+
static String encodeCloseQuote(FormatDelimited defn, String string) {
167+
if (!Strings.isNullOrEmpty(defn.getCloseQuote())) {
168+
String replacement = null;
169+
if (!Strings.isNullOrEmpty(defn.getEscapeCloseQuote())) {
170+
replacement = defn.getEscapeCloseQuote() + defn.getCloseQuote();
171+
} else if (!Strings.isNullOrEmpty(defn.getReplaceCloseQuote())) {
172+
replacement = defn.getReplaceCloseQuote();
173+
}
174+
if (replacement != null) {
175+
replacement = Matcher.quoteReplacement(replacement);
176+
string = string.replaceAll(Pattern.quote(defn.getCloseQuote()), replacement);
177+
}
178+
}
179+
return string;
180+
}
181+
167182
@Override
168183
public Future<Void> initialize(PipelineExecutor executor, PipelineInstance pipeline, ReadStreamWithTypes input) {
169184
this.types = input.getTypes();

query-engine/src/main/java/uk/co/spudsoft/query/main/Version.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public final class Version {
3434
/**
3535
* The project version, as set in the Maven pom.xml.
3636
*/
37-
public static final String MAVEN_PROJECT_VERSION = "0.0.70-3-main";
37+
public static final String MAVEN_PROJECT_VERSION = "0.0.71-main";
3838

3939
private Version() {
4040
}

query-engine/src/test/java/uk/co/spudsoft/query/defn/FormatDelimitedTest.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,11 @@ public void testBuilder() {
4141
assertEquals("\"", FormatDelimited.builder().build().getCloseQuote());
4242
assertEquals("$", FormatDelimited.builder().closeQuote("$").build().getCloseQuote());
4343

44-
assertEquals("\"", FormatDelimited.builder().build().getEscapeCloseQuote());
44+
assertEquals("", FormatDelimited.builder().build().getEscapeCloseQuote());
4545
assertEquals("$", FormatDelimited.builder().escapeCloseQuote("$").build().getEscapeCloseQuote());
46+
47+
assertEquals("", FormatDelimited.builder().build().getReplaceCloseQuote());
48+
assertEquals("$", FormatDelimited.builder().replaceCloseQuote("$").build().getReplaceCloseQuote());
4649
}
4750

4851
@Test
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright (C) 2025 njt
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
package uk.co.spudsoft.query.exec.fmts.text;
18+
19+
import static org.junit.jupiter.api.Assertions.assertEquals;
20+
import org.junit.jupiter.api.Test;
21+
import uk.co.spudsoft.query.defn.FormatDelimited;
22+
23+
/**
24+
*
25+
* @author njt
26+
*/
27+
public class FormatDelimitedInstanceTest {
28+
29+
@Test
30+
public void testEncodeCloseQuote() {
31+
assertEquals("bob", FormatDelimitedInstance.encodeCloseQuote(FormatDelimited.builder().build(), "bob"));
32+
assertEquals("\\\"bob\\\"", FormatDelimitedInstance.encodeCloseQuote(FormatDelimited.builder().escapeCloseQuote("\\").build(), "\"bob\""));
33+
assertEquals("&quot;bob&quot;", FormatDelimitedInstance.encodeCloseQuote(FormatDelimited.builder().replaceCloseQuote("&quot;").build(), "\"bob\""));
34+
}
35+
36+
}

0 commit comments

Comments
 (0)