Skip to content

Commit bd619b9

Browse files
committed
Merge scijava/scijava-plugins-io-table history
This migrates org.scijava.table.DefaultTableIOPlugin here. The scijava-plugins-io-table component will be retired. It has no extra dependencies. There used to be a CSV-based plugin in that component as well, but it has since been removed. There is no reason, currently, to maintain scijava-plugins-io-table separately.
2 parents ef89199 + ca9ad4f commit bd619b9

2 files changed

Lines changed: 677 additions & 0 deletions

File tree

Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
/*
2+
* #%L
3+
* I/O plugins for SciJava table objects.
4+
* %%
5+
* Copyright (C) 2017 - 2020 Board of Regents of the University of
6+
* Wisconsin-Madison and University of Konstanz.
7+
* %%
8+
* Redistribution and use in source and binary forms, with or without
9+
* modification, are permitted provided that the following conditions are met:
10+
*
11+
* 1. Redistributions of source code must retain the above copyright notice,
12+
* this list of conditions and the following disclaimer.
13+
* 2. Redistributions in binary form must reproduce the above copyright notice,
14+
* this list of conditions and the following disclaimer in the documentation
15+
* and/or other materials provided with the distribution.
16+
*
17+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
21+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27+
* POSSIBILITY OF SUCH DAMAGE.
28+
* #L%
29+
*/
30+
31+
package org.scijava.table;
32+
33+
import java.io.IOException;
34+
import java.util.ArrayList;
35+
import java.util.Arrays;
36+
import java.util.Collections;
37+
import java.util.HashMap;
38+
import java.util.HashSet;
39+
import java.util.List;
40+
import java.util.Map;
41+
import java.util.Set;
42+
import java.util.function.Function;
43+
44+
import org.scijava.Priority;
45+
import org.scijava.io.AbstractIOPlugin;
46+
import org.scijava.io.handle.DataHandle;
47+
import org.scijava.io.handle.DataHandleService;
48+
import org.scijava.io.location.Location;
49+
import org.scijava.plugin.Parameter;
50+
import org.scijava.plugin.Plugin;
51+
import org.scijava.table.io.ColumnTableIOOptions;
52+
import org.scijava.table.io.TableIOOptions;
53+
import org.scijava.table.io.TableIOPlugin;
54+
import org.scijava.util.FileUtils;
55+
56+
/**
57+
* Plugin for reading/writing {@link GenericTable}s.
58+
*
59+
* @author Leon Yang
60+
*/
61+
@SuppressWarnings("rawtypes")
62+
@Plugin(type = TableIOPlugin.class, priority = Priority.LOW)
63+
public class DefaultTableIOPlugin extends AbstractIOPlugin<Table> implements TableIOPlugin {
64+
65+
@Parameter
66+
private DataHandleService dataHandleService;
67+
68+
// FIXME: The "txt" extension is extremely general and will conflict with
69+
// other plugins. Consider another way to check supportsOpen/Close.
70+
private static final Set<String> SUPPORTED_EXTENSIONS = Collections
71+
.unmodifiableSet(new HashSet<>(Arrays.asList("csv", "txt", "prn", "dif",
72+
"rtf")));
73+
74+
@Override
75+
public boolean supportsOpen(final Location source) {
76+
final String ext = FileUtils.getExtension(source.getName()).toLowerCase();
77+
return SUPPORTED_EXTENSIONS.contains(ext);
78+
}
79+
80+
@Override
81+
public boolean supportsOpen(final String source) {
82+
final String ext = FileUtils.getExtension(source).toLowerCase();
83+
return SUPPORTED_EXTENSIONS.contains(ext);
84+
}
85+
86+
@Override
87+
public boolean supportsSave(Object data, String destination) {
88+
return supports(destination) && Table.class.isAssignableFrom(data.getClass());
89+
}
90+
91+
@Override
92+
public boolean supportsSave(final Location source) {
93+
return supportsOpen(source);
94+
}
95+
96+
@Override
97+
public boolean supportsSave(final String source) {
98+
return supportsOpen(source);
99+
}
100+
101+
/**
102+
* Process a given line into a list of tokens.
103+
*/
104+
private ArrayList<String> processRow(final String line, char separator, char quote) throws IOException {
105+
final ArrayList<String> row = new ArrayList<>();
106+
final StringBuilder sb = new StringBuilder();
107+
int idx = 0;
108+
int start = idx;
109+
while (idx < line.length()) {
110+
if (line.charAt(idx) == quote) {
111+
sb.append(line, start, idx);
112+
boolean quoted = true;
113+
idx++;
114+
start = idx;
115+
// find quoted string
116+
while (idx < line.length()) {
117+
if (line.charAt(idx) == quote) {
118+
sb.append(line, start, idx);
119+
if (idx + 1 < line.length() && line.charAt(idx + 1) == quote) {
120+
sb.append(quote);
121+
idx += 2;
122+
start = idx;
123+
}
124+
else {
125+
idx++;
126+
start = idx;
127+
quoted = false;
128+
break;
129+
}
130+
}
131+
else {
132+
idx++;
133+
}
134+
}
135+
if (quoted) {
136+
throw new IOException(String.format(
137+
"Unbalanced quote at position %d: %s", idx, line));
138+
}
139+
}
140+
else if (line.charAt(idx) == separator) {
141+
sb.append(line, start, idx);
142+
row.add(sb.toString());
143+
sb.setLength(0);
144+
idx++;
145+
start = idx;
146+
}
147+
else {
148+
idx++;
149+
}
150+
}
151+
sb.append(line, start, idx);
152+
row.add(sb.toString());
153+
return row;
154+
}
155+
156+
@Override
157+
public GenericTable open(final Location source, TableIOOptions options) throws IOException {
158+
return open(source, options.values);
159+
}
160+
161+
private GenericTable open(final Location source, TableIOOptions.Values options) throws IOException {
162+
163+
final GenericTable table = new DefaultGenericTable();
164+
165+
try (final DataHandle<? extends Location> handle = //
166+
dataHandleService.create(source))
167+
{
168+
if (!handle.exists()) {
169+
throw new IOException("Cannot open source");
170+
}
171+
long length = handle.length();
172+
173+
final byte[] buffer = new byte[(int) length];
174+
handle.read(buffer);
175+
176+
final String text = new String(buffer);
177+
178+
final char separator = options.columnDelimiter();
179+
final char quote = options.quote();
180+
boolean readRowHeaders = options.readRowHeaders();
181+
boolean readColHeaders = options.readColumnHeaders();
182+
183+
// split by any line delimiter
184+
final String[] lines = text.split("\\R");
185+
if (lines.length == 0) return table;
186+
// process first line to get number of cols
187+
Map<Integer, Function<String, ?>> columnParsers = new HashMap<>();
188+
{
189+
final ArrayList<String> tokens = processRow(lines[0], separator, quote);
190+
if (readColHeaders) {
191+
final List<String> colHeaders;
192+
if (readRowHeaders) colHeaders = tokens.subList(1, tokens.size());
193+
else colHeaders = tokens;
194+
final String[] colHeadersArr = new String[colHeaders.size()];
195+
table.appendColumns(colHeaders.toArray(colHeadersArr));
196+
}
197+
else {
198+
final List<String> cols;
199+
if (readRowHeaders) {
200+
cols = tokens.subList(1, tokens.size());
201+
table.appendColumns(cols.size());
202+
table.appendRow(tokens.get(0));
203+
}
204+
else {
205+
cols = tokens;
206+
table.appendColumns(cols.size());
207+
table.appendRow();
208+
}
209+
for (int i = 0; i < cols.size(); i++) {
210+
Function<String, ?> parser = getParser(cols.get(i), i, options);
211+
columnParsers.put(i, parser);
212+
table.set(i, 0, parser.apply(cols.get(i)));
213+
}
214+
}
215+
}
216+
for (int lineNum = 1; lineNum < lines.length; lineNum++) {
217+
final String line = lines[lineNum];
218+
final ArrayList<String> tokens = processRow(line, separator, quote);
219+
final List<String> cols;
220+
if (readRowHeaders) {
221+
cols = tokens.subList(1, tokens.size());
222+
table.appendRow(tokens.get(0));
223+
}
224+
else {
225+
cols = tokens;
226+
table.appendRow();
227+
}
228+
if (cols.size() != table.getColumnCount()) {
229+
throw new IOException("Line " + table.getRowCount() +
230+
" is not the same length as the first line.");
231+
}
232+
for (int i = 0; i < cols.size(); i++) {
233+
if(lineNum == 1 && readColHeaders) {
234+
columnParsers.put(i, getParser(cols.get(i), i, options));
235+
}
236+
table.set(i, lineNum - 1, columnParsers.get(i).apply(cols.get(i)));
237+
}
238+
}
239+
}
240+
return table;
241+
}
242+
243+
private static Function<String, ?> getParser(String content, int column, TableIOOptions.Values options) {
244+
ColumnTableIOOptions.Values colOptions = options.column(column);
245+
if(colOptions != null) return colOptions.parser();
246+
if(options.guessParser()) return guessParser(content);
247+
return options.parser();
248+
}
249+
250+
static Function<String, ?> guessParser(String content) {
251+
try {
252+
Function<String, ?> function = s -> Double.valueOf(s
253+
.replace("infinity", "Infinity")
254+
.replace("Nan", "NaN")
255+
);
256+
function.apply(content);
257+
return function;
258+
} catch(NumberFormatException ignored) {}
259+
if(content.equalsIgnoreCase("true")||content.equalsIgnoreCase("false")) {
260+
return Boolean::valueOf;
261+
}
262+
return String::valueOf;
263+
}
264+
265+
@Override
266+
public void save(final Table table, final Location destination, final TableIOOptions options)
267+
throws IOException {
268+
save(table, destination, options.values);
269+
}
270+
271+
private void save(final Table table, final Location destination, final TableIOOptions.Values options)
272+
throws IOException {
273+
274+
try (final DataHandle<Location> handle = //
275+
dataHandleService.create(destination))
276+
{
277+
final boolean writeRH = options.writeRowHeaders();
278+
final boolean writeCH = options.writeColumnHeaders();
279+
final char separator = options.columnDelimiter();
280+
final String eol = options.rowDelimiter();
281+
final char quote = options.quote();
282+
283+
final StringBuilder sb = new StringBuilder();
284+
// write column headers
285+
if (writeCH) {
286+
if (writeRH) {
287+
sb.append(tryQuote(options.cornerText(), separator, quote));
288+
if (table.getColumnCount() > 0) {
289+
sb.append(separator);
290+
sb.append(tryQuote(table.getColumnHeader(0), separator, quote));
291+
}
292+
}
293+
// avoid adding extra separator when there is 0 column
294+
else if (table.getColumnCount() > 0) {
295+
sb.append(tryQuote(table.getColumnHeader(0), separator, quote));
296+
}
297+
for (int col = 1; col < table.getColumnCount(); col++) {
298+
sb.append(separator);
299+
sb.append(tryQuote(table.getColumnHeader(col), separator, quote));
300+
}
301+
sb.append(eol);
302+
handle.writeBytes(sb.toString());
303+
sb.setLength(0);
304+
}
305+
// write each row
306+
for (int row = 0; row < table.getRowCount(); row++) {
307+
Function<Object, String> formatter = getFormatter(options, 0);
308+
if (writeRH) {
309+
sb.append(tryQuote(table.getRowHeader(row), separator, quote));
310+
if (table.getColumnCount() > 0) {
311+
sb.append(separator);
312+
sb.append(tryQuote(formatter.apply(table.get(0, row)), separator, quote));
313+
}
314+
}
315+
// avoid adding extra separator when there is 0 column
316+
else if (table.getColumnCount() > 0) {
317+
sb.append(tryQuote(formatter.apply(table.get(0, row)), separator, quote));
318+
}
319+
for (int col = 1; col < table.getColumnCount(); col++) {
320+
formatter = getFormatter(options, col);
321+
sb.append(separator);
322+
sb.append(tryQuote(formatter.apply(table.get(col, row)), separator, quote));
323+
}
324+
sb.append(eol);
325+
handle.writeBytes(sb.toString());
326+
sb.setLength(0);
327+
}
328+
}
329+
330+
}
331+
332+
private Function<Object, String> getFormatter(TableIOOptions.Values options, int i) {
333+
ColumnTableIOOptions.Values columnOptions = options.column(i);
334+
if(columnOptions != null) return columnOptions.formatter();
335+
return options.formatter();
336+
}
337+
338+
/**
339+
* Try to quote a string if:
340+
* <li>it is null or empty</li>
341+
* <li>it has quotes inside</li>
342+
* <li>it has separators or EOL inside</li>
343+
*
344+
* @param str string to quote
345+
* @return string, possibly quoted
346+
*/
347+
private String tryQuote(final String str, char separator, char quote) {
348+
if (str == null || str.length() == 0) return "" + quote + quote;
349+
if (str.indexOf(quote) != -1) return quote + str.replace("" + quote, "" +
350+
quote + quote) + quote;
351+
if (str.indexOf(separator) != -1) return quote + str + quote;
352+
return str;
353+
}
354+
}

0 commit comments

Comments
 (0)