• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Nanopublication / nanopub-java / 23530534266

25 Mar 2026 07:50AM UTC coverage: 51.341% (+0.008%) from 51.333%
23530534266

push

github

tkuhn
fix: disable OpenCSV backslash escape in CSV parsing

OpenCSV's default escape character is '\', which causes backslashes in
API response values to be silently stripped (e.g. "\n" becomes "n"). The
query API uses standard RFC 4180 CSV with doubled quotes for escaping,
not backslashes. Disabling the escape char preserves literal backslashes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

1030 of 2990 branches covered (34.45%)

Branch coverage included in aggregate %.

5250 of 9242 relevant lines covered (56.81%)

7.98 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.41
src/main/java/org/nanopub/extra/services/QueryAccess.java
1
package org.nanopub.extra.services;
2

3
import com.opencsv.CSVParserBuilder;
4
import com.opencsv.CSVReader;
5
import com.opencsv.CSVReaderBuilder;
6
import com.opencsv.CSVWriterBuilder;
7
import com.opencsv.ICSVParser;
8
import com.opencsv.ICSVWriter;
9
import com.opencsv.exceptions.CsvValidationException;
10
import org.apache.http.HttpResponse;
11
import org.eclipse.rdf4j.model.Model;
12
import org.eclipse.rdf4j.rio.RDFFormat;
13
import org.eclipse.rdf4j.rio.Rio;
14

15
import java.io.BufferedReader;
16
import java.io.IOException;
17
import java.io.InputStreamReader;
18
import java.io.Writer;
19

20
/**
21
 * Second-generation query API access
22
 */
23
public abstract class QueryAccess {
9✔
24

25
    /**
26
     * Process the header.
27
     *
28
     * @param line the header line from the CSV response
29
     */
30
    protected abstract void processHeader(String[] line);
31

32
    /**
33
     * Process a line of data from the CSV response.
34
     *
35
     * @param line the line of data from the CSV response
36
     */
37
    protected abstract void processLine(String[] line);
38

39
    /**
40
     * Process the RDF content from a SPARQL CONSTRUCT query response.
41
     * Override this method to handle RDF responses. Default implementation is a no-op.
42
     *
43
     * @param model the parsed RDF model
44
     */
45
    protected void processRdfContent(Model model) {
46
    }
×
47

48
    /**
49
     * Call a query with the given queryId and parameters.
50
     *
51
     * @param queryRef the query reference
52
     * @throws FailedApiCallException         if the API call fails
53
     * @throws APINotReachableException       if the API is not reachable
54
     * @throws NotEnoughAPIInstancesException if there are not enough API instances available
55
     */
56
    public void call(QueryRef queryRef) throws FailedApiCallException, APINotReachableException, NotEnoughAPIInstancesException {
57
        HttpResponse resp = QueryCall.run(queryRef);
9✔
58
        String contentType = resp.getFirstHeader("Content-Type") != null
12!
59
                ? resp.getFirstHeader("Content-Type").getValue() : "";
18✔
60
        String mimeType = contentType.contains(";") ? contentType.split(";")[0].trim() : contentType.trim();
42✔
61
        if (mimeType.equals("text/csv") || mimeType.isEmpty()) {
21!
62
            try (CSVReader csvReader = new CSVReaderBuilder(new BufferedReader(new InputStreamReader(resp.getEntity().getContent())))
48✔
63
                    .withCSVParser(new CSVParserBuilder().withEscapeChar(ICSVParser.NULL_CHARACTER).build())
9✔
64
                    .build()) {
6✔
65
                String[] line = null;
6✔
66
                int n = 0;
6✔
67
                while ((line = csvReader.readNext()) != null) {
15✔
68
                    n++;
3✔
69
                    if (n == 1) {
9✔
70
                        processHeader(line);
12✔
71
                    } else {
72
                        processLine(line);
12✔
73
                    }
74
                }
75
            } catch (IOException | CsvValidationException ex) {
×
76
                throw new FailedApiCallException(ex);
×
77
            }
3✔
78
        } else {
79
            RDFFormat format = Rio.getParserFormatForMIMEType(mimeType).orElse(RDFFormat.TURTLE);
18✔
80
            try {
81
                Model model = Rio.parse(resp.getEntity().getContent(), format);
24✔
82
                processRdfContent(model);
9✔
83
            } catch (IOException ex) {
×
84
                throw new FailedApiCallException(ex);
×
85
            }
3✔
86
        }
87
    }
3✔
88

89
    /**
90
     * Print the response of a query in CSV format to the given writer.
91
     *
92
     * @param queryRef the query reference
93
     * @param writer   the writer to print the CSV response to
94
     * @throws FailedApiCallException         if the API call fails
95
     * @throws APINotReachableException       if the API is not reachable
96
     * @throws NotEnoughAPIInstancesException if there are not enough API instances available
97
     */
98
    public static void printCvsResponse(QueryRef queryRef, Writer writer) throws FailedApiCallException, APINotReachableException, NotEnoughAPIInstancesException {
99
        ICSVWriter icsvWriter = new CSVWriterBuilder(writer).withSeparator(',').build();
×
100
        QueryAccess a = new QueryAccess() {
×
101

102
            @Override
103
            protected void processHeader(String[] line) {
104
                icsvWriter.writeNext(line);
×
105
            }
×
106

107
            @Override
108
            protected void processLine(String[] line) {
109
                icsvWriter.writeNext(line);
×
110
            }
×
111

112
        };
113
        a.call(queryRef);
×
114
        icsvWriter.flushQuietly();
×
115
    }
×
116

117
    /**
118
     * Get the response of a query as an ApiResponse object.
119
     *
120
     * @param queryRef the query reference
121
     * @return an ApiResponse object containing the response data
122
     * @throws FailedApiCallException         if the API call fails
123
     * @throws APINotReachableException       if the API is not reachable
124
     * @throws NotEnoughAPIInstancesException if there are not enough API instances available
125
     */
126
    public static ApiResponse get(QueryRef queryRef) throws FailedApiCallException, APINotReachableException, NotEnoughAPIInstancesException {
127
        final ApiResponse response = new ApiResponse();
12✔
128
        QueryAccess a = new QueryAccess() {
33✔
129

130
            @Override
131
            protected void processHeader(String[] line) {
132
                response.setHeader(line);
12✔
133
            }
3✔
134

135
            @Override
136
            protected void processLine(String[] line) {
137
                response.add(line);
12✔
138
            }
3✔
139

140
            @Override
141
            protected void processRdfContent(Model model) {
142
                response.setRdfContent(model);
12✔
143
            }
3✔
144

145
        };
146
        a.call(queryRef);
9✔
147
        return response;
6✔
148
    }
149

150
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc