Java tutorial
/* * Copyright (c) 2013 Philippe VIENNE * * This file is a part of SpeleoGraph * * SpeleoGraph is free software: you can redistribute * it and/or modify it under the terms of the GNU General * Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your * option) any later version. * * SpeleoGraph is distributed in the hope that it will * be useful, but WITHOUT ANY WARRANTY; without even the * implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU General Public License * for more details. * * You should have received a copy of the GNU General Public * License along with SpeleoGraph. * If not, see <http://www.gnu.org/licenses/>. */ package org.cds06.speleograph.data.fileio; import au.com.bytecode.opencsv.CSVReader; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOCase; import org.apache.commons.io.filefilter.*; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.cds06.speleograph.I18nSupport; import org.cds06.speleograph.data.Item; import org.cds06.speleograph.data.Series; import org.cds06.speleograph.data.Type; import org.cds06.speleograph.graph.DrawStyle; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jfree.chart.axis.NumberAxis; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.awt.*; import java.io.*; import java.text.DecimalFormat; import java.text.NumberFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*; /** * This file is created by PhilippeGeek. * Distributed on licence GNU GPL V3. */ public class SpeleoFileReader implements DataFileReader { @SuppressWarnings("UnusedDeclaration") @NonNls private static final Logger log = LoggerFactory.getLogger(SpeleoFileReader.class); /** * This exception is thrown when try to read a non-speleoGraph file. */ @NonNls public static final FileReadingError NOT_SPELEO_FILE = new FileReadingError( "This file is not a SpeleoGraph File", FileReadingError.Part.HEAD); /** * This string must match to the first line of any SpeleoGraph File. */ @NonNls public static final String SPELEOGRAPH_FILE_HEADER = "SpeleoGraph File"; private static final int READING_HEADERS = 0; private static final int FINDING_HEADERS = -1; private static final int READING_DATA = 1; private static final int CHECKING = 2; private static DataFileReader instance = new SpeleoFileReader(); public static DataFileReader getInstance() { return instance; } private ArrayList<NumberAxis> axes = new ArrayList<>(); private ArrayList<Boolean> typeAxesChecker = new ArrayList<>(); /** * Read a file with SpeleoGraph File Format. * * @param file The file to read * @throws FileReadingError On error while reading the file */ @SuppressWarnings("HardCodedStringLiteral") @Override public void readFile(File file) throws FileReadingError { InputStreamReader streamReader; try { streamReader = new InputStreamReader(new FileInputStream(file), "UTF-8"); // NON-NLS } catch (UnsupportedEncodingException | FileNotFoundException e) { log.error("Can not access to file", e); throw new FileReadingError(I18nSupport.translate("error.canNotOpenFile", file.getName()), FileReadingError.Part.HEAD, e); } CSVReader reader = new CSVReader(streamReader, ';', '"'); axes = new ArrayList<>(); typeAxesChecker = new ArrayList<>(); String[] line; try { line = reader.readNext(); } catch (IOException e) { throw new FileReadingError(I18nSupport.translate("error.canNotReadFileOrEmpty"), FileReadingError.Part.HEAD, e); } int size, state = CHECKING; HeaderInformation headers = new HeaderInformation(); DateInformation date = new DateInformation(); headers.setDateInformation(date); while (line != null) { size = line.length; if (size == 0) { log.info("Empty line while reading file, just continue our walk."); continue; } String firstLineElement = line[0]; if (firstLineElement.equals("eof")) break; // Force end for reading NON-NLS switch (state) { case CHECKING: if (!SPELEOGRAPH_FILE_HEADER.equals(firstLineElement)) throw NOT_SPELEO_FILE; state = FINDING_HEADERS; break; case FINDING_HEADERS: if ("headers".equals(firstLineElement)) state = READING_HEADERS; // NON-NLS break; case READING_HEADERS: switch (firstLineElement) { case "data": state = READING_DATA; break; case "date": readDateHeaderLine(date, line); break; case "axis": try { NumberAxis axis = new NumberAxis(line[2]); axis.setLowerBound((DecimalFormat.getInstance().parse(line[3])).doubleValue()); axis.setUpperBound(DecimalFormat.getInstance().parse(line[4]).doubleValue()); typeAxesChecker.add(Integer.parseInt(line[1]), new Properties(line).getBoolean("type")); axes.add(Integer.parseInt(line[1]), axis); } catch (Exception e) { log.error("Can not read axis", e); } break; case "chart": break; default: readSeriesHeaderLine(file, line, headers); } break; case READING_DATA: if (size <= 1) break; headers.read(line); break; default: log.info("State error in reading"); } try { line = reader.readNext(); } catch (IOException e) { log.debug("None next lines", e); } } Series.notifyInstanceListeners(); log.info("File reading is ended"); } @NonNls @Override public String getName() { return "SpeleoGraph"; } @Override public String getButtonText() { return I18nSupport.translate("actions.openFile"); } private static final AndFileFilter filter = new AndFileFilter(); static { filter.addFileFilter(FileFileFilter.FILE); filter.addFileFilter(CanReadFileFilter.CAN_READ); filter.addFileFilter(CanWriteFileFilter.CAN_WRITE); filter.addFileFilter(EmptyFileFilter.NOT_EMPTY); filter.addFileFilter(new SuffixFileFilter(new String[] { ".speleo", ".csv", ".txt" }, IOCase.INSENSITIVE));// NON-NLS filter.addFileFilter(new IOFileFilter() { @Override public boolean accept(File file) { try { return new Scanner(file).nextLine().equals(SPELEOGRAPH_FILE_HEADER); } catch (Exception e) { return false; } } @Override public boolean accept(File dir, String name) { return accept(FileUtils.getFile(dir, name)); } }); } /** * Get the FileFilter to use. * * @return A file filter */ @NotNull @Override public IOFileFilter getFileFilter() { return filter; } /** * Read a header line to add series in headers information. * <p>I must write to you the english doc for series lines</p> * * @param file The file used to extract the data * @param line The parsed line * @param headers The object which represent the headers */ private void readSeriesHeaderLine(File file, String[] line, HeaderInformation headers) { int size = line.length, column = Integer.parseInt(line[0]); if (size < 3) { // A series line must have a length gather than 2 log.info("Invalid header : " + StringUtils.join(line, ' ')); return; } @NonNls Properties p = new Properties(line); Type t = Type.getType(line[1], line[2]); Series series = new Series(file, t); { if (p.getBoolean("show")) series.setShow(true); if (p.getBoolean("stepped")) series.setStepped(true); if (p.get("style") != null) { String style = p.get("style"); for (DrawStyle s : DrawStyle.values()) { if (s.toString().equals(style)) series.setStyle(s); } } if (p.get("color") != null) series.setColor(new Color(Integer.parseInt(p.get("color")))); if (p.get("name") != null) { series.setName(p.get("name")); } if (p.getNumber("axis") != null) { Integer id = p.getNumber("axis"); if (typeAxesChecker.get(id)) { t.setAxis(axes.get(id)); } else { series.setAxis(axes.get(id)); } } } if (p.getBoolean("min-max")) { Integer min = p.getNumber("min"), max = p.getNumber("max"); if (min == null || max == null) return; if (headers.hasSeriesForColumn(min) && headers.hasSeriesForColumn(max)) { series.delete(); } series.setMinMax(true); headers.set(series, min, max); } else { headers.set(series, column); } } /** * Read a header line as a date line. * * @param date The date information where add the date parsing information * @param line The parsed line from the CSV */ private static void readDateHeaderLine(DateInformation date, String[] line) { int size = line.length; if (size < 4) { log.info("Invalid header : " + StringUtils.join(line, ' ')); throw new IllegalStateException("Invalid date entry"); } String time = line[1]; if (!time.isEmpty()) date.setTimeZone(TimeZone.getTimeZone(time)); for (int i = 2; i < (size - 1); i = i + 2) date.set(Integer.parseInt(line[i]), line[i + 1]); } /** * Read a file into Series. * <p>Series are stored into the {@link HeaderInformation}. This function will call it line by line to push the data * into series. In case of error, we simply continue to the next value.</p> * * @param headers This object contains all data usefull * @param file The file which we will read * @throws FileNotFoundException If file does not exists. * @throws IOException If an error occurs when read line in the file, to get more information about this exception * see {@link au.com.bytecode.opencsv.CSVReader#readNext()}. */ public static void read(File file, HeaderInformation headers) throws IOException { Validate.notNull(file); Validate.notNull(headers); final CSVReader reader = new CSVReader(new java.io.FileReader(file), headers.getColumnSeparator(), '"'); int lineId = -1; String[] line; while ((line = reader.readNext()) != null) { lineId++; if (lineId < headers.getFirstLineOfData()) continue; headers.read(line); } } /** * Store date-column link information for a file. * <p>Each column can be link to a java date format scheme. When we read an entry, we join all columns for date and * all date formats and ask to Java to parse it. If we got an error on parse, just return the actual date.</p> */ public static class DateInformation { /** * Columns joined to create the date to parse. */ private int[] columns = new int[0]; /** * The date format for a column with the same array index. * <p>Ex.: dateFormats[i] is the format for columns[i]</p> */ private String[] dateFormats = new String[0]; /** * This is the final computed value from {@link DateInformation#dateFormats}. */ private String dateFormat = "dd/MM/yyyy HH:mm:ss"; private SimpleDateFormat format = new SimpleDateFormat(dateFormat); private TimeZone timeZone; protected String computeDateFormat() { dateFormat = StringUtils.join(dateFormats, ' '); format = new SimpleDateFormat(dateFormat); if (timeZone != null) format.setTimeZone(timeZone); return dateFormat; } public int set(int column, String format) { Validate.notNull(column); Validate.notNull(format); Validate.notEmpty(format); Validate.isTrue(column >= 0, "Column index should be positive"); { // Check if format is a valid date format try { new SimpleDateFormat(format).format(Calendar.getInstance().getTime()); } catch (IllegalArgumentException e) { return -1; } } int index = ArrayUtils.indexOf(columns, column); boolean isAdding = false; if (index == ArrayUtils.INDEX_NOT_FOUND) { index = columns.length; columns = Arrays.copyOf(columns, index + 1); dateFormats = Arrays.copyOf(dateFormats, index + 1); isAdding = true; } columns[index] = column; dateFormats[index] = format; computeDateFormat(); return isAdding ? 1 : 0; } public Date parse(String[] line) { String[] toJoin = new String[columns.length]; for (int i = 0; i < columns.length; i++) { toJoin[i] = line[columns[i]]; } String date = StringUtils.join(toJoin, ' '); try { return format.parse(date); } catch (ParseException e) { throw new IllegalStateException("Can not parse a date !", e); } } /** * Determine if a column is already linked to a date format. * * @param index The column index * @return True if a date format exist. */ public boolean hasDateInformationForColumn(int index) { return ArrayUtils.contains(columns, index); } /** * Get the current format for a column * * @param column The column to find * @return The format or null if this column has no date format. */ public String getForColumn(int column) { for (int i = 0; i < columns.length; i++) if (columns[i] == column) return dateFormats[i]; return null; } /** * Delete an entry. * * @param index The index of column to delete */ public void remove(int index) { dateFormats = ArrayUtils.removeElement(dateFormats, getForColumn(index)); columns = ArrayUtils.removeElement(columns, index); } public void setTimeZone(TimeZone timeZone) { this.timeZone = timeZone; } } /** * Information about a file header. * <p>A file header is a pack of information which are which series we will read, which columns are liked to a * series, what are the date columns, how to parse the date ...</p> * <p>This class is designed to be used by two classes, the first is {@link ImportWizard} which will populate this * class with user information, the second is {@link SpeleoFileReader} which will use it to parse the file fast.</p> * * @author Philippe VIENNE * @see java.io.Serializable This class is serializable to be saved in case we have to reuse it. * @since 1.0 */ public static class HeaderInformation implements Serializable { /** * Serial Version UID. * Don't forget to change it if you change the serialization methods. */ static final long serialVersionUID = 1L; /** * Number format is used to parse numbers in columns. */ private static final NumberFormat numberFormat = NumberFormat.getNumberInstance(); /** * Number of read Series. * This value is computed to go faster while iterating series or columns. It must be a computed value and never * be visible for elements outside this class. */ protected int numberOfSeriesToParse; /** * Series array, it contains all series to read mapped to an id (the index). */ protected Series[] series = new Series[0]; /** * Columns array, contains the columns id to read for a Series. * <p>Each entry in this array is designed as the following rules : * <ul> * <li>{@code [int]}: is a single column data and series admit only a value per item</li> * <li>{@code [int, int]}: each item has a minimal value (first index) and maximal value(second index)</li> * <li>{@code [int, int, int]}: 0 is a value, 1 a minimal value and 2 a maximal value</li> * </ul> * each entry in this array which has null length or length gather than 3 will be ignored. */ protected Integer[][] columns = new Integer[0][]; /** * The line in the file where we will start to read data. */ protected int firstLineOfData; /** * Information about date columns. * <p>A column used for a Date element should not be used for a data.</p> */ protected DateInformation dateInformation; /** * Separator for each column in this file. */ private char columnSeparator = ';'; /** * Get the value of the line in the file where we will start to read data. * * @return An integer if not it will be a RuntimeError. */ public int getFirstLineOfData() { return firstLineOfData; } /** * Set the value of the line in the file where we will start to read data. * * @param firstLineOfData A non-negative integer * @throws IllegalArgumentException if the argument is not an integer or a negative integer. */ public void setFirstLineOfData(int firstLineOfData) { Validate.notNull(firstLineOfData, "The argument should not be null."); Validate.isTrue(firstLineOfData >= 0, "The argument should be positive"); this.firstLineOfData = firstLineOfData; } /** * Set the date information for the current file. * * @param dateInformation A non null object which represent the date information. */ public void setDateInformation(DateInformation dateInformation) { Validate.notNull(dateInformation); this.dateInformation = dateInformation; } /** * Determine if a column is already linked to a series. * * @return true if a Series exist for this column. */ public boolean hasSeriesForColumn(int index) { for (Integer[] cols : columns) if (ArrayUtils.contains(cols, index)) return true; return false; } /** * Get a series for an index. * * @param index The series index */ public Series getSeries(int index) { Validate.validIndex(series, index, "The index should correspond to a series entry"); return series[index]; } /** * Get a series for a column. * * @param column The column index */ @Nullable public Series getSeriesForColumn(int column) { int index = -1; for (int i = 0; i < columns.length; i++) { if (ArrayUtils.contains(columns, column)) { index = i; break; } } return index == -1 ? null : getSeries(index); } /** * Get column information for a series. * * @param series The series * @return The column information or null if series is not found in this header. */ public Integer[] getColumnInformation(Series series) { final int index = ArrayUtils.indexOf(this.series, series); if (index == ArrayUtils.INDEX_NOT_FOUND) return null; Validate.validIndex(columns, index, "No column data for the index %d", index); return columns[index]; } /** * Set data information for a Series and Columns. * * @param series The series to add (should be not null) * @param column The column information with this format: <ul> * <li>{@code [int]}: is a single column data and series admit only a value per item</li> * <li>{@code [int, int]}: each item has a minimal value (first index) and maximal value(second index)</li> * <li>{@code [int, int, int]}: 0 is a value, 1 a minimal value and 2 a maximal value</li> * </ul> * @return 0 if it creates a new entry, 1 if it update one, -1 in case of error. */ public int set(Series series, Integer... column) { Validate.notNull(series); Validate.notEmpty(column); Validate.isTrue(column.length < 4, "Column array should have a length between 1 and 3 (see documentation)"); int index = ArrayUtils.indexOf(this.series, series); boolean isCreated = false; if (index == ArrayUtils.INDEX_NOT_FOUND) { // Must remove other attached series to columns for (int col : column) { if (hasSeriesForColumn(col)) { remove(col); } } index = this.series.length; this.series = Arrays.copyOf(this.series, index + 1); this.columns = Arrays.copyOf(this.columns, index + 1); this.numberOfSeriesToParse = this.series.length; isCreated = true; } this.series[index] = series; this.columns[index] = column; return isCreated ? 0 : 1; } /** * Read a line of data. * * @param line The array of columns. Should not be null. * @return 0 if parse is full correct, 1 otherwise. */ public int read(String[] line) { try { final Date date = dateInformation.parse(line); for (int i = 0; i < numberOfSeriesToParse; i++) { final Integer[] columnIds = columns[i]; Item item; if (columnIds.length == 1) { if ("".equals(line[columnIds[0]])) continue; item = new Item(series[i], date, numberFormat.parse(line[columnIds[0]]).doubleValue()); } else if (columnIds.length == 2) { if ("".equals(line[columnIds[0]])) continue; if ("".equals(line[columnIds[1]])) continue; item = new Item(series[i], date, numberFormat.parse(line[columnIds[0]]).doubleValue(), numberFormat.parse(line[columnIds[1]]).doubleValue()); } else { continue; } series[i].add(item); } return 0; } catch (Exception e) { log.error("Can not read an entry", e); return 1; } } public char getColumnSeparator() { return columnSeparator; } public void setColumnSeparator(char columnSeparator) { this.columnSeparator = columnSeparator; } public void remove(int index) { for (int i = 0; i < columns.length; i++) { for (int c : columns[i]) { if (c == index) { columns = ArrayUtils.remove(columns, i); series = ArrayUtils.remove(series, i); } } } } } /** * Class used to parse and represent properties read from a SpeleoGraph File. * * @author Philippe VIENNE * @since 1.0 */ private static class Properties { /** * Store properties. */ private HashMap<String, String> properties = new HashMap<>(); /** * Create properties from an array. * * @param properties Each value in the array is a [key]:[value]. */ public Properties(String[] properties) { for (String p : properties) { final String[] strings = StringUtils.split(p, ":", 2); if (strings.length != 2) continue; this.properties.put(strings[0].toLowerCase(), strings[1]); } } /** * Get a value as a boolean. * "0", false, null are considered as false, all other values are true * * @param key The key to find * @return the value corresponding to the key as a boolean. */ public boolean getBoolean(String key) { @NonNls String v = properties.get(key); return v != null && !(v.equals("0") || v.equals("false") || v.equals("non") || v.equals("N") || v.equals("F")); } /** * Get a value as an Integer * * @param key The key to find * @return the numerical value * @see Integer#parseInt(String) */ public Integer getNumber(String key) { try { return Integer.parseInt(properties.get(key)); } catch (Throwable throwable) { return null; } } /** * Get a value. * * @param key The key to find * @return the value in properties */ public String get(String key) { return properties.get(key); } } }