/*
* Copyright 2011-16 Fraunhofer ISE
*
* This file is part of OpenMUC.
* For more information visit http://www.openmuc.org
*
* OpenMUC 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.
*
* OpenMUC 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 OpenMUC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.openmuc.framework.datalogger.ascii;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import org.openmuc.framework.data.ByteArrayValue;
import org.openmuc.framework.data.DoubleValue;
import org.openmuc.framework.data.Flag;
import org.openmuc.framework.data.Record;
import org.openmuc.framework.data.StringValue;
import org.openmuc.framework.datalogger.ascii.utils.Const;
import org.openmuc.framework.datalogger.ascii.utils.LoggerUtils;
import org.openmuc.framework.datalogger.spi.LogChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogFileReader {
private static final Logger logger = LoggerFactory.getLogger(LogFileReader.class);
private final String channelId;
private final String path;
private final int loggingInterval;
private final int logTimeOffset;
private int unixTimestampColumn;
private long startTimestamp;
private long endTimestamp;
private long firstTimestampFromFile;
/**
* LogFileReader Constructor
*
* @param path
* the path to the files to read from
* @param logChannel
* the channel to read from
*/
public LogFileReader(String path, LogChannel logChannel) {
this.path = path;
channelId = logChannel.getId();
this.loggingInterval = logChannel.getLoggingInterval();
this.logTimeOffset = logChannel.getLoggingTimeOffset();
firstTimestampFromFile = -1;
}
/**
* Get the values between start time stamp and end time stamp
*
* @param startTimestamp
* start time stamp
* @param endTimestamp
* end time stamp
* @return All records of the given time span
*/
public List<Record> getValues(long startTimestamp, long endTimestamp) {
this.startTimestamp = startTimestamp;
this.endTimestamp = endTimestamp;
List<Record> allRecords = new ArrayList<>();
List<String> filenames = LoggerUtils.getFilenames(loggingInterval, logTimeOffset, this.startTimestamp,
this.endTimestamp);
for (int i = 0; i < filenames.size(); i++) {
Boolean nextFile = false;
if (logger.isTraceEnabled()) {
logger.trace("using " + filenames.get(i));
}
String filepath;
if (path.endsWith(File.separator)) {
filepath = path + filenames.get(i);
}
else {
filepath = path + File.separatorChar + filenames.get(i);
}
if (i > 0) {
nextFile = true;
}
List<Record> fileRecords = processFile(filepath, nextFile);
if (fileRecords != null) {
allRecords.addAll(fileRecords);
if (logger.isTraceEnabled()) {
logger.trace("read records: " + fileRecords.size());
}
}
else {
// some error occurred while processing the file so no records will be added
}
}
return allRecords;
}
/**
* get a single record of time stamp
*
* @param timestamp
* time stamp
* @return Record on success, otherwise null
*/
public Record getValue(long timestamp) {
// Returns a record which lays within the interval [timestamp, timestamp + loggingInterval]
// The interval is necessary for a requested time stamp which lays between the time stamps of two logged values
// e.g.: t_request = 7, t1_logged = 5, t2_logged = 10, loggingInterval = 5
// method will return the record of t2_logged because this lays within the interval [7,12]
// If the t_request matches exactly a logged time stamp, then the according record is returned.
Record record = null;
List<Record> records = getValues(timestamp, timestamp);// + loggingInterval);
if (records.size() == 0) {
// no record found for requested timestamp
// TODO statt null flag 3 setzen!
record = null;
}
else if (records.size() == 1) {
// t_request lays between two logged values
record = records.get(0);
}
else if (records.size() == 2) {
// t_request matches exactly a logged value
// so getVaules returns a record for t_request and one for t_request+loggingInterval
record = records.get(0);
}
// nur wert zurückgeben wenn zeitstempel identisch ist
// sonst
return record;
}
/**
* Reads the file line by line
*
* @param filepath
* file path
* @param nextFile
* if it is the next file and not the first between a time span
* @return records on success, otherwise null
*/
private List<Record> processFile(String filepath, Boolean nextFile) {
List<Record> records = new ArrayList<>();
String line = null;
long currentPosition = 0;
long rowSize;
long firstTimestamp = 0;
String firstValueLine = null;
long currentTimestamp = 0;
RandomAccessFile raf = LoggerUtils.getRandomAccessFile(new File(filepath), "r");
if (raf == null) {
return null;
}
try {
int channelColumn = -1;
while (channelColumn <= 0) {
line = raf.readLine();
channelColumn = LoggerUtils.getColumnNumberByName(line, channelId);
unixTimestampColumn = LoggerUtils.getColumnNumberByName(line, Const.TIMESTAMP_STRING);
}
firstValueLine = raf.readLine();
rowSize = firstValueLine.length() + 1; // +1 because of "\n"
// rewind the position to the start of the firstValue line
currentPosition = raf.getFilePointer() - rowSize;
firstTimestamp = (long) (Double.valueOf((firstValueLine.split(Const.SEPARATOR))[unixTimestampColumn])
* 1000);
if (nextFile || startTimestamp < firstTimestamp) {
startTimestamp = firstTimestamp;
}
if (startTimestamp >= firstTimestamp) {
long filepos = getFilePosition(loggingInterval, startTimestamp, firstTimestamp, currentPosition,
rowSize);
raf.seek(filepos);
currentTimestamp = startTimestamp;
while ((line = raf.readLine()) != null && currentTimestamp <= endTimestamp) {
processLine(line, channelColumn, records);
currentTimestamp += loggingInterval;
}
raf.close();
}
else {
records = null; // because the column of the channel was not identified
}
} catch (IOException e) {
e.printStackTrace();
records = null;
}
return records;
}
/**
* Process the line: ignore comments, read records
*
* @param line
* the line to process
* @param channelColumn
* channel column
* @param records
* list of records
*/
private void processLine(String line, int channelColumn, List<Record> records) {
if (!line.startsWith(Const.COMMENT_SIGN)) {
readRecordFromLine(line, channelColumn, records);
}
}
/**
* Reads records from a line.
*
* @param line
* to read
* @param column
* of the channelId
* @return Record read from line
*/
private void readRecordFromLine(String line, int channelColumn, List<Record> records) {
String columnValue[] = line.split(Const.SEPARATOR);
try {
Double timestampS = Double.parseDouble(columnValue[unixTimestampColumn]);
long timestampMS = ((Double) (timestampS * (1000))).longValue();
if (isTimestampPartOfRequestedInterval(timestampMS)) {
Record record = convertLogfileEntryToRecord(columnValue[channelColumn].trim(), timestampMS);
records.add(record);
}
else {
// for debugging
// SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS");
// logger.trace("timestampMS: " + sdf.format(timestampMS) + " " + timestampMS);
}
} catch (NumberFormatException e) {
logger.debug("It's not a timestamp.");
e.printStackTrace();
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
}
}
/**
* Checks if the time stamp read from file is part of the requested logging interval
*
* @param lineTimestamp
* time stamp to check if it is part of the time span
* @return true if it is a part of the requested interval, if not false.
*/
private boolean isTimestampPartOfRequestedInterval(long lineTimestamp) {
boolean result = false;
// TODO tidy up, move to better place, is asked each line!
if (firstTimestampFromFile == -1) {
firstTimestampFromFile = lineTimestamp;
}
if (lineTimestamp >= startTimestamp && lineTimestamp <= endTimestamp) {
result = true;
}
return result;
}
/**
* Get the position of the startTimestamp, without Header.
*
* @param loggingInterval
* logging interval
* @param startTimestamp
* start time stamp
* @return the position of the start timestamp as long.
*/
private long getFilePosition(int loggingInterval, long startTimestamp, long firstTimestampOfFile,
long firstValuePos, long rowSize) {
long timeOffsetMs = startTimestamp - firstTimestampOfFile;
long numberOfLinesToSkip = timeOffsetMs / loggingInterval;
// if offset isn't a multiple of loggingInterval add an additional line
if (timeOffsetMs % loggingInterval != 0) {
++numberOfLinesToSkip;
}
long pos = numberOfLinesToSkip * rowSize + firstValuePos;
// for debugging
// logger.trace("pos " + pos);
// logger.trace("startTimestamp " + startTimestamp);
// logger.trace("firstTimestamp " + firstTimestampOfFile);
// logger.trace("loggingInterval " + loggingInterval);
// logger.trace("rowSize " + rowSize);
// logger.trace("firstValuePos " + firstValuePos);
return pos;
}
// TODO support ints, booleans, ...
/**
* Converts an entry from the logging file into a record
*
* @param strValue
* string value
* @param timestamp
* time stamp
* @return the converted logfile entry.
*/
private Record convertLogfileEntryToRecord(String strValue, long timestamp) {
Record record = null;
if (isNumber(strValue)) {
record = new Record(new DoubleValue(Double.parseDouble(strValue)), timestamp, Flag.VALID);
}
else {
// fehlerfall, wenn errors "errxx" geloggt wurden
// record = new Record(null, timestamp, Flag);
record = getRecordFromNonNumberValue(strValue, timestamp);
}
return record;
}
/**
* Returns the record from a non number value read from the logfile. This is the case if the value is an error like
* "e0" or a normal ByteArrayValue
*
* @param strValue
* string value
* @param timestamp
* time stamp
* @return the value in a record.
*/
private Record getRecordFromNonNumberValue(String strValue, long timestamp) {
Record record = null;
if (strValue.trim().startsWith(Const.ERROR)) {
int errorSize = Const.ERROR.length();
int stringLength = strValue.length();
String errorFlag = strValue.substring(errorSize, errorSize + stringLength - errorSize);
errorFlag = errorFlag.trim();
if (isNumber(errorFlag)) {
record = new Record(null, timestamp, Flag.newFlag(Integer.parseInt(errorFlag)));
}
else {
record = new Record(null, timestamp, Flag.NO_VALUE_RECEIVED_YET);
}
}
else if (strValue.trim().startsWith(Const.HEXADECIMAL)) {
try {
record = new Record(new ByteArrayValue(strValue.trim().getBytes(Const.CHAR_SET)), timestamp,
Flag.VALID);
} catch (UnsupportedEncodingException e) {
record = new Record(Flag.UNKNOWN_ERROR);
logger.error("Hexadecimal value is non US-ASCII decoded, value is: " + strValue.trim());
}
}
else {
record = new Record(new StringValue(strValue.trim()), timestamp, Flag.VALID);
}
return record;
}
/**
* Checks if the string value is a number
*
* @param strValue
* string value
* @return True on success, otherwise false
*/
private boolean isNumber(String strValue) {
boolean isDecimalSeparatorFound = false;
if (!Character.isDigit(strValue.charAt(0)) && strValue.charAt(0) != Const.MINUS_SIGN
&& strValue.charAt(0) != Const.PLUS_SIGN) {
return false;
}
for (char charactor : strValue.substring(1).toCharArray()) {
if (!Character.isDigit(charactor)) {
if (charactor == Const.DECIMAL_SEPARATOR && !isDecimalSeparatorFound) {
isDecimalSeparatorFound = true;
continue;
}
return false;
}
}
return true;
}
}