/*
* 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.slotsdb;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.List;
import java.util.Vector;
import org.openmuc.framework.data.DoubleValue;
import org.openmuc.framework.data.Flag;
import org.openmuc.framework.data.Record;
public final class FileObject {
private long startTimeStamp; // byte 0-7 in file (cached)
private long storagePeriod; // byte 8-15 in file (cached)
private final File dataFile;
private DataOutputStream dos;
private BufferedOutputStream bos;
private FileOutputStream fos;
private DataInputStream dis;
private FileInputStream fis;
private boolean canWrite;
private boolean canRead;
/*
* File length will be cached to avoid system calls an improve I/O Performance
*/
private long length = 0;
public FileObject(String filename) throws IOException {
canWrite = false;
canRead = false;
dataFile = new File(filename);
length = dataFile.length();
if (dataFile.exists() && length >= 16) {
/*
* File already exists -> get file Header (startTime and step-frequency) TODO: compare to starttime and
* frequency in constructor! new file needed? update to file-array!
*/
try {
fis = new FileInputStream(dataFile);
try {
dis = new DataInputStream(fis);
try {
startTimeStamp = dis.readLong();
storagePeriod = dis.readLong();
} finally {
if (dis != null) {
dis.close();
dis = null;
}
}
} finally {
if (dis != null) {
dis.close();
dis = null;
}
}
} finally {
if (fis != null) {
fis.close();
fis = null;
}
}
}
}
public FileObject(File file) throws IOException {
canWrite = false;
canRead = false;
dataFile = file;
length = dataFile.length();
if (dataFile.exists() && length >= 16) {
/*
* File already exists -> get file Header (startTime and step-frequency)
*/
fis = new FileInputStream(dataFile);
try {
dis = new DataInputStream(fis);
try {
startTimeStamp = dis.readLong();
storagePeriod = dis.readLong();
} finally {
if (dis != null) {
dis.close();
dis = null;
}
}
} finally {
if (fis != null) {
fis.close();
fis = null;
}
}
}
}
private void enableOutput() throws IOException {
/*
* Close Input Streams, for enabling output.
*/
if (dis != null) {
dis.close();
dis = null;
}
if (fis != null) {
fis.close();
fis = null;
}
/*
* enabling output
*/
if (fos == null || dos == null || bos == null) {
fos = new FileOutputStream(dataFile, true);
bos = new BufferedOutputStream(fos);
dos = new DataOutputStream(bos);
}
canRead = false;
canWrite = true;
}
private void enableInput() throws IOException {
/*
* Close Output Streams for enabling input.
*/
if (dos != null) {
dos.flush();
dos.close();
dos = null;
}
if (bos != null) {
bos.close();
bos = null;
}
if (fos != null) {
fos.close();
fos = null;
}
/*
* enabling input
*/
if (fis == null || dis == null) {
fis = new FileInputStream(dataFile);
dis = new DataInputStream(fis);
}
canWrite = false;
canRead = true;
}
/**
* Return the Timestamp of the first stored Value in this File.
*
* @return timestamp as long
*/
public long getStartTimeStamp() {
return startTimeStamp;
}
/**
* Returns the step frequency in seconds.
*
* @return step frequency in seconds
*/
public long getStoringPeriod() {
return storagePeriod;
}
/**
* creates the file, if it doesn't exist.
*
* @param startTimeStamp
* for file header
* @param stepIntervall
* for file header
* @throws IOException
* if an I/O error occurs.
*/
public void createFileAndHeader(long startTimeStamp, long stepIntervall) throws IOException {
if (!dataFile.exists() || length < 16) {
dataFile.getParentFile().mkdirs();
if (dataFile.exists() && length < 16) {
dataFile.delete(); // file corrupted (header shorter that 16
}
// bytes)
dataFile.createNewFile();
this.startTimeStamp = startTimeStamp;
storagePeriod = stepIntervall;
/*
* Do not close Output streams, because after writing the header -> data will follow!
*/
fos = new FileOutputStream(dataFile);
bos = new BufferedOutputStream(fos);
dos = new DataOutputStream(bos);
dos.writeLong(startTimeStamp);
dos.writeLong(stepIntervall);
dos.flush();
length += 16; /* wrote 2*8 Bytes */
canWrite = true;
canRead = false;
}
}
public void append(double value, long timestamp, byte flag) throws IOException {
long writePosition = getBytePosition(timestamp);
if (writePosition == length) {
/*
* value for this timeslot has not been saved yet "AND" some value has been stored in last timeslot
*/
if (!canWrite) {
enableOutput();
}
dos.writeDouble(value);
dos.writeByte(flag);
length += 9;
}
else {
if (length > writePosition) {
/*
* value has already been stored for this timeslot -> handle? AVERAGE, MIN, MAX, LAST speichern?!
*/
}
else {
/*
* there are missing some values missing -> fill up with NaN!
*/
if (!canWrite) {
enableOutput();
}
long rowsToFillWithNan = (writePosition - length) / 9;// TODO:
// stimmt
// Berechnung?
for (int i = 0; i < rowsToFillWithNan; i++) {
dos.writeDouble(Double.NaN); // TODO: festlegen welcher Wert
// undefined sein soll NaN
// ok?
dos.writeByte(Flag.NO_VALUE_RECEIVED_YET.getCode()); // TODO:
// festlegen
// welcher Wert
// undefined sein
// soll 00 ok?
length += 9;
}
dos.writeDouble(value);
dos.writeByte(flag);
length += 9;
}
}
/*
* close(); OutputStreams will not be closed or flushed. Data will be written to disk after calling flush()
* method.
*/
}
public long getTimestampForLatestValue() {
return startTimeStamp + (((length - 16) / 9) - 1) * storagePeriod;
}
/**
* calculates the position in a file for a certain timestamp
*
* @param timestamp
* the searched timestamp
* @return position the position of the timestamp
*/
private long getBytePosition(long timestamp) {
if (timestamp >= startTimeStamp) {
/*
* get position for timestamp 117 000: 117 000 - 100 000 = 17 000 17 * 000 / 5 000 = 3.4 Math.round(3.4) = 3
* 3*(8+1) = 27 27 + 16 = 43 = position to store to!
*/
// long pos = (Math.round((double) (timestamp - startTimeStamp) /
// storagePeriod) * 9) + 16; /* slower */
double pos = (double) (timestamp - startTimeStamp) / storagePeriod;
if (pos % 1 != 0) { /* faster */
pos = Math.round(pos);
}
return (long) (pos * 9 + 16);
}
else {
// not in file! should never happen...
return -1;
}
}
/*
* Calculates the closest timestamp to wanted timestamp getByteposition does a similar thing (Math.round()), for
* byte position.
*/
private long getClosestTimestamp(long timestamp) {
// return Math.round((double) (timestamp -
// startTimeStamp)/storagePeriod)*storagePeriod+startTimeStamp; /*
// slower */
double ts = (double) (timestamp - startTimeStamp) / storagePeriod;
if (ts % 1 != 0) {
ts = Math.round(ts);
}
return (long) ts * storagePeriod + startTimeStamp;
}
public Record read(long timestamp) throws IOException {
timestamp = getClosestTimestamp(timestamp); // round to: startTimestamp
// + n*stepIntervall
if (timestamp >= startTimeStamp && timestamp <= getTimestampForLatestValue()) {
if (!canRead) {
enableInput();
}
fis.getChannel().position(getBytePosition(timestamp));
Double toReturn = dis.readDouble();
if (!Double.isNaN(toReturn)) {
return new Record(new DoubleValue(toReturn), timestamp, Flag.newFlag(dis.readByte()));
}
}
return null;
}
/**
* Returns a List of Value Objects containing the measured Values between provided start and end timestamp
*
* @param start
* start timestamp
* @param end
* end timestamp
* @return a list of records
* @throws IOException
* if an I/O error occurs.
*/
public List<Record> read(long start, long end) throws IOException {
start = getClosestTimestamp(start); // round to: startTimestamp +
// n*stepIntervall
end = getClosestTimestamp(end); // round to: startTimestamp +
// n*stepIntervall
List<Record> toReturn = new Vector<>();
if (start < end) {
if (start < startTimeStamp) {
// of this file.
start = startTimeStamp;
}
if (end > getTimestampForLatestValue()) {
end = getTimestampForLatestValue();
}
if (!canRead) {
enableInput();
}
long timestampcounter = start;
long startPos = getBytePosition(start);
long endPos = getBytePosition(end);
fis.getChannel().position(startPos);
byte[] b = new byte[(int) (endPos - startPos) + 9];
dis.read(b, 0, b.length);
ByteBuffer bb = ByteBuffer.wrap(b);
bb.rewind();
for (int i = 0; i <= (endPos - startPos) / 9; i++) {
double d = bb.getDouble();
Flag s = Flag.newFlag(bb.get());
if (!Double.isNaN(d)) {
toReturn.add(new Record(new DoubleValue(d), timestampcounter, s));
}
timestampcounter += storagePeriod;
}
}
else if (start == end) {
toReturn.add(read(start));
toReturn.removeAll(Collections.singleton(null));
}
return toReturn; // Always return a list -> might be empty -> never is
// null, to avoid NP's
}
public List<Record> readFully() throws IOException {
return read(startTimeStamp, getTimestampForLatestValue());
}
/**
* Closes and Flushes underlying Input- and OutputStreams
*
* @throws IOException
* if an I/O error occurs.
*/
public void close() throws IOException {
canRead = false;
canWrite = false;
if (dos != null) {
dos.flush();
dos.close();
dos = null;
}
if (fos != null) {
fos.close();
fos = null;
}
if (dis != null) {
dis.close();
dis = null;
}
if (fis != null) {
fis.close();
fis = null;
}
}
/**
* Flushes the underlying Data Streams.
*
* @throws IOException
* if an I/O error occurs.
*/
public void flush() throws IOException {
if (dos != null) {
dos.flush();
}
}
}