Skip to content

Commit 521c631

Browse files
committed
Migrate implementations from imagej-server
This adds @lnyng's implementation of IOPlugin<GenericTable> as well as tests and a benchmark.
1 parent 3773f7e commit 521c631

3 files changed

Lines changed: 614 additions & 1 deletion

File tree

pom.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,13 @@ Institute of Molecular Cell Biology and Genetics.</license.copyrightOwners>
105105
<artifactId>commons-csv</artifactId>
106106
<version>1.3</version>
107107
</dependency>
108-
108+
109+
<!-- SCIFIO dependencies -->
110+
<dependency>
111+
<groupId>io.scif</groupId>
112+
<artifactId>scifio</artifactId>
113+
</dependency>
114+
109115
<!-- Test scope dependencies -->
110116
<dependency>
111117
<groupId>junit</groupId>
Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
/*
2+
* #%L
3+
* ImageJ server for RESTful access to ImageJ.
4+
* %%
5+
* Copyright (C) 2013 - 2016 Board of Regents of the University of
6+
* Wisconsin-Madison.
7+
* %%
8+
* Licensed under the Apache License, Version 2.0 (the "License");
9+
* you may not use this file except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing, software
15+
* distributed under the License is distributed on an "AS IS" BASIS,
16+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* See the License for the specific language governing permissions and
18+
* limitations under the License.
19+
* #L%
20+
*/
21+
22+
package net.imagej.table;
23+
24+
import io.scif.io.IRandomAccess;
25+
import io.scif.io.VirtualHandle;
26+
import io.scif.services.LocationService;
27+
28+
import java.io.IOException;
29+
import java.util.ArrayList;
30+
import java.util.Arrays;
31+
import java.util.Collections;
32+
import java.util.HashSet;
33+
import java.util.List;
34+
import java.util.Set;
35+
import java.util.function.Function;
36+
import java.util.stream.IntStream;
37+
38+
import org.scijava.Priority;
39+
import org.scijava.io.AbstractIOPlugin;
40+
import org.scijava.io.IOPlugin;
41+
import org.scijava.plugin.Parameter;
42+
import org.scijava.plugin.Plugin;
43+
import org.scijava.util.FileUtils;
44+
45+
/**
46+
* Plugin for reading/writing {@link GenericTable}s.
47+
*
48+
* @author Leon Yang
49+
*/
50+
@Plugin(type = IOPlugin.class, priority = Priority.LOW_PRIORITY)
51+
public class DefaultTableIOPlugin extends AbstractIOPlugin<GenericTable> {
52+
53+
@Parameter
54+
private LocationService locationService;
55+
56+
/** Reads the first row of the input file as column headers. */
57+
@Parameter(required = false)
58+
private boolean readColHeaders = true;
59+
60+
/** Writes column headers to file if there exists at least one. */
61+
@Parameter(required = false)
62+
private boolean writeColHeaders = true;
63+
64+
/** Reads the first column of the input file as row headers. */
65+
@Parameter(required = false)
66+
private boolean readRowHeaders = false;
67+
68+
/** Writes row headers to file if there exists at least one. */
69+
@Parameter(required = false)
70+
private boolean writeRowHeaders = true;
71+
72+
/** Regex pattern that separates cells in each row of the table. */
73+
@Parameter(required = false)
74+
private char separator = ',';
75+
76+
/** End of line when writing to file. */
77+
@Parameter(required = false)
78+
private String eol = System.lineSeparator();
79+
80+
/**
81+
* Quote character used for escaping separator and empty strings. Use two
82+
* consecutive quotes to escape one.
83+
*/
84+
@Parameter(required = false)
85+
private char quote = '"';
86+
87+
/**
88+
* Text that appears at the top left corner when both column and row headers
89+
* present.
90+
*/
91+
@Parameter(required = false)
92+
private String cornerText = "\\";
93+
94+
/**
95+
* Lambda function that converts the string of a cell to an appropriate value.
96+
*/
97+
@Parameter(required = false)
98+
private Function<String, Object> parser = s -> s;
99+
100+
/** Lambda function that convert the cell content to a string. */
101+
@Parameter(required = false)
102+
private Function<Object, String> formatter = o -> o.toString();
103+
104+
// FIXME: The "txt" extension is extremely general and will conflict with
105+
// other plugins. Consider another way to check supportsOpen/Close.
106+
private static final Set<String> SUPPORTED_EXTENSIONS = Collections
107+
.unmodifiableSet(new HashSet<>(Arrays.asList("csv", "txt", "prn", "dif",
108+
"rtf")));
109+
110+
@Override
111+
public Class<GenericTable> getDataType() {
112+
return GenericTable.class;
113+
}
114+
115+
@Override
116+
public boolean supportsOpen(final String source) {
117+
final String ext = FileUtils.getExtension(source).toLowerCase();
118+
return SUPPORTED_EXTENSIONS.contains(ext);
119+
}
120+
121+
@Override
122+
public boolean supportsSave(final String source) {
123+
return supportsOpen(source);
124+
}
125+
126+
/**
127+
* Process a given line into a list of tokens.
128+
*/
129+
private ArrayList<String> processRow(final String line) throws IOException {
130+
final ArrayList<String> row = new ArrayList<>();
131+
final StringBuilder sb = new StringBuilder();
132+
int idx = 0;
133+
int start = idx;
134+
while (idx < line.length()) {
135+
if (line.charAt(idx) == quote) {
136+
sb.append(line.substring(start, idx));
137+
boolean quoted = true;
138+
idx++;
139+
start = idx;
140+
// find quoted string
141+
while (idx < line.length()) {
142+
if (line.charAt(idx) == quote) {
143+
sb.append(line.substring(start, idx));
144+
if (idx + 1 < line.length() && line.charAt(idx + 1) == quote) {
145+
sb.append(quote);
146+
idx += 2;
147+
start = idx;
148+
}
149+
else {
150+
idx++;
151+
start = idx;
152+
quoted = false;
153+
break;
154+
}
155+
}
156+
else {
157+
idx++;
158+
}
159+
}
160+
if (quoted) {
161+
throw new IOException(String.format(
162+
"Unbalanced quote at position %d: %s", idx, line));
163+
}
164+
}
165+
else if (line.charAt(idx) == separator) {
166+
sb.append(line.substring(start, idx));
167+
row.add(sb.toString());
168+
sb.setLength(0);
169+
idx++;
170+
start = idx;
171+
}
172+
else {
173+
idx++;
174+
}
175+
}
176+
sb.append(line.substring(start, idx));
177+
row.add(sb.toString());
178+
return row;
179+
}
180+
181+
@Override
182+
public GenericTable open(final String source) throws IOException {
183+
final IRandomAccess handle = locationService.getHandle(source);
184+
if (handle instanceof VirtualHandle) {
185+
throw new IOException("Cannot open source");
186+
}
187+
handle.seek(0);
188+
final byte[] buffer = new byte[(int) handle.length()];
189+
handle.read(buffer);
190+
final String text = new String(buffer);
191+
192+
final GenericTable table = new DefaultGenericTable();
193+
194+
// split by any line delimiter
195+
final String[] lines = text.split("\\R");
196+
if (lines.length == 0) return table;
197+
// process first line to get number of cols
198+
{
199+
final ArrayList<String> tokens = processRow(lines[0]);
200+
if (readColHeaders) {
201+
final List<String> colHeaders;
202+
if (readRowHeaders) colHeaders = tokens.subList(1, tokens.size());
203+
else colHeaders = tokens;
204+
final String[] colHeadersArr = new String[colHeaders.size()];
205+
table.appendColumns(colHeaders.toArray(colHeadersArr));
206+
}
207+
else {
208+
final List<String> cols;
209+
if (readRowHeaders) {
210+
cols = tokens.subList(1, tokens.size());
211+
table.appendColumns(cols.size());
212+
table.appendRow(tokens.get(0));
213+
}
214+
else {
215+
cols = tokens;
216+
table.appendColumns(cols.size());
217+
table.appendRow();
218+
}
219+
for (int i = 0; i < cols.size(); i++) {
220+
table.set(i, 0, parser.apply(cols.get(i)));
221+
}
222+
}
223+
}
224+
for (int lineNum = 1; lineNum < lines.length; lineNum++) {
225+
final String line = lines[lineNum];
226+
final ArrayList<String> tokens = processRow(line);
227+
final List<String> cols;
228+
if (readRowHeaders) {
229+
cols = tokens.subList(1, tokens.size());
230+
table.appendRow(tokens.get(0));
231+
}
232+
else {
233+
cols = tokens;
234+
table.appendRow();
235+
}
236+
if (cols.size() != table.getColumnCount()) {
237+
throw new IOException("Line " + table.getRowCount() +
238+
" is not the same length as the first line.");
239+
}
240+
for (int i = 0; i < cols.size(); i++) {
241+
table.set(i, lineNum - 1, parser.apply(cols.get(i)));
242+
}
243+
}
244+
return table;
245+
}
246+
247+
@Override
248+
public void save(final GenericTable table, final String source)
249+
throws IOException
250+
{
251+
final IRandomAccess handle = locationService.getHandle(source, true);
252+
if (handle instanceof VirtualHandle) {
253+
throw new IOException("Cannot open source");
254+
}
255+
handle.seek(0);
256+
257+
final boolean writeRH = this.writeRowHeaders && table.getRowCount() > 0 &&
258+
IntStream.range(0, table.getRowCount()).allMatch(row -> table
259+
.getRowHeader(row) != null);
260+
final boolean writeCH = this.writeColHeaders && table
261+
.getColumnCount() > 0 && table.stream().allMatch(col -> col
262+
.getHeader() != null);
263+
264+
final StringBuilder sb = new StringBuilder();
265+
// write column headers
266+
if (writeCH) {
267+
if (writeRH) {
268+
sb.append(tryQuote(cornerText));
269+
if (table.getColumnCount() > 0) {
270+
sb.append(separator);
271+
sb.append(tryQuote(table.getColumnHeader(0)));
272+
}
273+
}
274+
// avoid adding extra separator when there is 0 column
275+
else if (table.getColumnCount() > 0) {
276+
sb.append(tryQuote(table.getColumnHeader(0)));
277+
}
278+
for (int col = 1; col < table.getColumnCount(); col++) {
279+
sb.append(separator);
280+
sb.append(tryQuote(table.getColumnHeader(col)));
281+
}
282+
sb.append(eol);
283+
handle.writeBytes(sb.toString());
284+
sb.setLength(0);
285+
}
286+
// write each row
287+
for (int row = 0; row < table.getRowCount(); row++) {
288+
if (writeRH) {
289+
sb.append(tryQuote(table.getRowHeader(row)));
290+
if (table.getColumnCount() > 0) {
291+
sb.append(separator);
292+
sb.append(tryQuote(formatter.apply(table.get(0, row))));
293+
}
294+
}
295+
// avoid adding extra separator when there is 0 column
296+
else if (table.getColumnCount() > 0) {
297+
sb.append(tryQuote(formatter.apply(table.get(0, row))));
298+
}
299+
for (int col = 1; col < table.getColumnCount(); col++) {
300+
sb.append(separator);
301+
sb.append(tryQuote(formatter.apply(table.get(col, row))));
302+
}
303+
sb.append(eol);
304+
handle.writeBytes(sb.toString());
305+
sb.setLength(0);
306+
}
307+
}
308+
309+
/**
310+
* Try to quote a string if:
311+
* <li>it is null or empty</li>
312+
* <li>it has quotes inside</li>
313+
* <li>it has separators or EOL inside</li>
314+
*
315+
* @param str string to quote
316+
* @return string, possibly quoted
317+
*/
318+
private String tryQuote(final String str) {
319+
if (str == null || str.length() == 0) return "" + quote + quote;
320+
if (str.indexOf(quote) != -1) return quote + str.replace("" + quote, "" +
321+
quote + quote) + quote;
322+
if (str.indexOf(separator) != -1) return quote + str + quote;
323+
return str;
324+
}
325+
}

0 commit comments

Comments
 (0)