3535import java .util .ArrayList ;
3636import java .util .Arrays ;
3737import java .util .Collections ;
38+ import java .util .HashMap ;
3839import java .util .HashSet ;
3940import java .util .List ;
41+ import java .util .Map ;
4042import java .util .Set ;
4143import java .util .function .Function ;
42- import java .util .stream .IntStream ;
4344
4445import org .scijava .Priority ;
45- import org .scijava .io .AbstractIOPlugin ;
4646import org .scijava .io .IOPlugin ;
4747import org .scijava .io .handle .DataHandle ;
4848import org .scijava .io .handle .DataHandleService ;
4949import org .scijava .io .location .Location ;
5050import org .scijava .io .location .LocationService ;
5151import org .scijava .plugin .Parameter ;
5252import org .scijava .plugin .Plugin ;
53+ import org .scijava .table .io .ColumnTableIOOptions ;
54+ import org .scijava .table .io .TableIOOptions ;
55+ import org .scijava .table .io .TableIOPlugin ;
5356import org .scijava .util .FileUtils ;
5457
5558/**
5962 */
6063@ SuppressWarnings ("rawtypes" )
6164@ Plugin (type = IOPlugin .class , priority = Priority .LOW )
62- public class DefaultTableIOPlugin extends AbstractIOPlugin < Table > {
65+ public class DefaultTableIOPlugin extends TableIOPlugin {
6366
6467 @ Parameter
6568 private LocationService locationService ;
6669
6770 @ Parameter
6871 private DataHandleService dataHandleService ;
6972
70- /** Reads the first row of the input file as column headers. */
71- @ Parameter (required = false )
72- private boolean readColHeaders = true ;
73-
74- /** Writes column headers to file if there exists at least one. */
75- @ Parameter (required = false )
76- private boolean writeColHeaders = true ;
77-
78- /** Reads the first column of the input file as row headers. */
79- @ Parameter (required = false )
80- private boolean readRowHeaders = false ;
81-
82- /** Writes row headers to file if there exists at least one. */
83- @ Parameter (required = false )
84- private boolean writeRowHeaders = true ;
85-
86- /** Regex pattern that separates cells in each row of the table. */
87- @ Parameter (required = false )
88- private char separator = ',' ;
89-
90- /** End of line when writing to file. */
91- @ Parameter (required = false )
92- private String eol = System .lineSeparator ();
93-
94- /**
95- * Quote character used for escaping separator and empty strings. Use two
96- * consecutive quotes to escape one.
97- */
98- @ Parameter (required = false )
99- private char quote = '"' ;
100-
101- /**
102- * Text that appears at the top left corner when both column and row headers
103- * present.
104- */
105- @ Parameter (required = false )
106- private String cornerText = "\\ " ;
107-
108- /**
109- * Lambda function that converts the string of a cell to an appropriate value.
110- */
111- @ Parameter (required = false )
112- private Function <String , Object > parser = s -> s ;
113-
114- /** Lambda function that convert the cell content to a string. */
115- @ Parameter (required = false )
116- private Function <Object , String > formatter = o -> o .toString ();
117-
11873 // FIXME: The "txt" extension is extremely general and will conflict with
11974 // other plugins. Consider another way to check supportsOpen/Close.
12075 private static final Set <String > SUPPORTED_EXTENSIONS = Collections
12176 .unmodifiableSet (new HashSet <>(Arrays .asList ("csv" , "txt" , "prn" , "dif" ,
12277 "rtf" )));
12378
124- @ Override
125- public Class <Table > getDataType () {
126- return Table .class ;
127- }
128-
12979 @ Override
13080 public boolean supportsOpen (final String source ) {
13181 final String ext = FileUtils .getExtension (source ).toLowerCase ();
@@ -140,21 +90,21 @@ public boolean supportsSave(final String source) {
14090 /**
14191 * Process a given line into a list of tokens.
14292 */
143- private ArrayList <String > processRow (final String line ) throws IOException {
93+ private ArrayList <String > processRow (final String line , char separator , char quote ) throws IOException {
14494 final ArrayList <String > row = new ArrayList <>();
14595 final StringBuilder sb = new StringBuilder ();
14696 int idx = 0 ;
14797 int start = idx ;
14898 while (idx < line .length ()) {
14999 if (line .charAt (idx ) == quote ) {
150- sb .append (line . substring ( start , idx ) );
100+ sb .append (line , start , idx );
151101 boolean quoted = true ;
152102 idx ++;
153103 start = idx ;
154104 // find quoted string
155105 while (idx < line .length ()) {
156106 if (line .charAt (idx ) == quote ) {
157- sb .append (line . substring ( start , idx ) );
107+ sb .append (line , start , idx );
158108 if (idx + 1 < line .length () && line .charAt (idx + 1 ) == quote ) {
159109 sb .append (quote );
160110 idx += 2 ;
@@ -177,7 +127,7 @@ private ArrayList<String> processRow(final String line) throws IOException {
177127 }
178128 }
179129 else if (line .charAt (idx ) == separator ) {
180- sb .append (line . substring ( start , idx ) );
130+ sb .append (line , start , idx );
181131 row .add (sb .toString ());
182132 sb .setLength (0 );
183133 idx ++;
@@ -187,13 +137,18 @@ else if (line.charAt(idx) == separator) {
187137 idx ++;
188138 }
189139 }
190- sb .append (line . substring ( start , idx ) );
140+ sb .append (line , start , idx );
191141 row .add (sb .toString ());
192142 return row ;
193143 }
194144
195145 @ Override
196- public GenericTable open (final String source ) throws IOException {
146+ public GenericTable open (final String source , TableIOOptions options ) throws IOException {
147+ return open (source , options .values );
148+ }
149+
150+ private GenericTable open (final String source , TableIOOptions .Values options ) throws IOException {
151+
197152 final Location sourceLocation ;
198153 try {
199154 sourceLocation = locationService .resolve (source );
@@ -216,13 +171,18 @@ public GenericTable open(final String source) throws IOException {
216171
217172 final String text = new String (buffer );
218173
174+ final char separator = options .columnDelimiter ();
175+ final char quote = options .quote ();
176+ boolean readRowHeaders = options .readRowHeaders ();
177+ boolean readColHeaders = options .readColumnHeaders ();
219178
220179 // split by any line delimiter
221180 final String [] lines = text .split ("\\ R" );
222181 if (lines .length == 0 ) return table ;
223182 // process first line to get number of cols
183+ Map <Integer , Function <String , Object >> columnParsers = new HashMap <>();
224184 {
225- final ArrayList <String > tokens = processRow (lines [0 ]);
185+ final ArrayList <String > tokens = processRow (lines [0 ], separator , quote );
226186 if (readColHeaders ) {
227187 final List <String > colHeaders ;
228188 if (readRowHeaders ) colHeaders = tokens .subList (1 , tokens .size ());
@@ -243,13 +203,15 @@ public GenericTable open(final String source) throws IOException {
243203 table .appendRow ();
244204 }
245205 for (int i = 0 ; i < cols .size (); i ++) {
206+ Function <String , Object > parser = getParser (cols .get (i ), i , options );
207+ columnParsers .put (i , parser );
246208 table .set (i , 0 , parser .apply (cols .get (i )));
247209 }
248210 }
249211 }
250212 for (int lineNum = 1 ; lineNum < lines .length ; lineNum ++) {
251213 final String line = lines [lineNum ];
252- final ArrayList <String > tokens = processRow (line );
214+ final ArrayList <String > tokens = processRow (line , separator , quote );
253215 final List <String > cols ;
254216 if (readRowHeaders ) {
255217 cols = tokens .subList (1 , tokens .size ());
@@ -264,17 +226,56 @@ public GenericTable open(final String source) throws IOException {
264226 " is not the same length as the first line." );
265227 }
266228 for (int i = 0 ; i < cols .size (); i ++) {
267- table .set (i , lineNum - 1 , parser .apply (cols .get (i )));
229+ if (lineNum == 1 && readColHeaders ) {
230+ columnParsers .put (i , getParser (cols .get (i ), i , options ));
231+ }
232+ table .set (i , lineNum - 1 , columnParsers .get (i ).apply (cols .get (i )));
268233 }
269234 }
270235 }
271236 return table ;
272237 }
273238
239+ private static Function <String , Object > getParser (String content , int column , TableIOOptions .Values options ) {
240+ ColumnTableIOOptions .Values colOptions = options .column (column );
241+ if (colOptions != null ) return colOptions .parser ();
242+ if (options .guessParser ()) return guessParser (content );
243+ return options .parser ();
244+ }
245+
246+ static Function <String , Object > guessParser (String content ) {
247+ try {
248+ Integer .valueOf (content );
249+ return Integer ::valueOf ;
250+ } catch (NumberFormatException ignored ) {}
251+ try {
252+ Long .valueOf (content );
253+ return Long ::valueOf ;
254+ } catch (NumberFormatException ignored ) {}
255+ try {
256+ Double .valueOf (content );
257+ return Double ::valueOf ;
258+ } catch (NumberFormatException ignored ) {}
259+ if (content .equalsIgnoreCase ("true" )||content .equalsIgnoreCase ("false" )) {
260+ return Boolean ::valueOf ;
261+ }
262+ return String ::valueOf ;
263+ }
264+
274265 @ Override
275266 public void save (final Table table , final String destination )
276- throws IOException
277- {
267+ throws IOException {
268+ save (table , destination , new TableIOOptions ().values );
269+ }
270+
271+ @ Override
272+ public void save (final Table table , final String destination , final TableIOOptions options )
273+ throws IOException {
274+ save (table , destination , options .values );
275+ }
276+
277+ private void save (final Table table , final String destination , final TableIOOptions .Values options )
278+ throws IOException {
278279 final Location dstLocation ;
279280 try {
280281 dstLocation = locationService .resolve (destination );
@@ -286,53 +287,52 @@ public void save(final Table table, final String destination)
286287 try (final DataHandle <Location > handle = //
287288 dataHandleService .create (dstLocation ))
288289 {
289- final boolean writeRH = this .writeRowHeaders && //
290- table .getRowCount () > 0 && //
291- IntStream .range (0 , table .getRowCount ()).allMatch (row -> table
292- .getRowHeader (row ) != null );
293- final boolean writeCH = this .writeColHeaders && //
294- table .getColumnCount () > 0 && //
295- IntStream .range (0 , table .getColumnCount ()).allMatch (col -> table
296- .getColumnHeader (col ) != null );
290+ final boolean writeRH = options .writeRowHeaders ();
291+ final boolean writeCH = options .writeColumnHeaders ();
292+ final char separator = options .columnDelimiter ();
293+ final String eol = options .rowDelimiter ();
294+ final char quote = options .quote ();
297295
298296 final StringBuilder sb = new StringBuilder ();
299297 // write column headers
300298 if (writeCH ) {
301299 if (writeRH ) {
302- sb .append (tryQuote (cornerText ));
300+ sb .append (tryQuote (options . cornerText (), separator , quote ));
303301 if (table .getColumnCount () > 0 ) {
304302 sb .append (separator );
305- sb .append (tryQuote (table .getColumnHeader (0 )));
303+ sb .append (tryQuote (table .getColumnHeader (0 ), separator , quote ));
306304 }
307305 }
308306 // avoid adding extra separator when there is 0 column
309307 else if (table .getColumnCount () > 0 ) {
310- sb .append (tryQuote (table .getColumnHeader (0 )));
308+ sb .append (tryQuote (table .getColumnHeader (0 ), separator , quote ));
311309 }
312310 for (int col = 1 ; col < table .getColumnCount (); col ++) {
313311 sb .append (separator );
314- sb .append (tryQuote (table .getColumnHeader (col )));
312+ sb .append (tryQuote (table .getColumnHeader (col ), separator , quote ));
315313 }
316314 sb .append (eol );
317315 handle .writeBytes (sb .toString ());
318316 sb .setLength (0 );
319317 }
320318 // write each row
321319 for (int row = 0 ; row < table .getRowCount (); row ++) {
320+ Function <Object , String > formatter = getFormatter (options , 0 );
322321 if (writeRH ) {
323- sb .append (tryQuote (table .getRowHeader (row )));
322+ sb .append (tryQuote (table .getRowHeader (row ), separator , quote ));
324323 if (table .getColumnCount () > 0 ) {
325324 sb .append (separator );
326- sb .append (tryQuote (formatter .apply (table .get (0 , row ))));
325+ sb .append (tryQuote (formatter .apply (table .get (0 , row )), separator , quote ));
327326 }
328327 }
329328 // avoid adding extra separator when there is 0 column
330329 else if (table .getColumnCount () > 0 ) {
331- sb .append (tryQuote (formatter .apply (table .get (0 , row ))));
330+ sb .append (tryQuote (formatter .apply (table .get (0 , row )), separator , quote ));
332331 }
333332 for (int col = 1 ; col < table .getColumnCount (); col ++) {
333+ formatter = getFormatter (options , col );
334334 sb .append (separator );
335- sb .append (tryQuote (formatter .apply (table .get (col , row ))));
335+ sb .append (tryQuote (formatter .apply (table .get (col , row )), separator , quote ));
336336 }
337337 sb .append (eol );
338338 handle .writeBytes (sb .toString ());
@@ -342,6 +342,12 @@ else if (table.getColumnCount() > 0) {
342342
343343 }
344344
345+ private Function <Object , String > getFormatter (TableIOOptions .Values options , int i ) {
346+ ColumnTableIOOptions .Values columnOptions = options .column (i );
347+ if (columnOptions != null ) return columnOptions .formatter ();
348+ return options .formatter ();
349+ }
350+
345351 /**
346352 * Try to quote a string if:
347353 * <li>it is null or empty</li>
@@ -351,7 +357,7 @@ else if (table.getColumnCount() > 0) {
351357 * @param str string to quote
352358 * @return string, possibly quoted
353359 */
354- private String tryQuote (final String str ) {
360+ private String tryQuote (final String str , char separator , char quote ) {
355361 if (str == null || str .length () == 0 ) return "" + quote + quote ;
356362 if (str .indexOf (quote ) != -1 ) return quote + str .replace ("" + quote , "" +
357363 quote + quote ) + quote ;
0 commit comments