/* TagRecorder.java created 2010-10-30 * */ package org.signalml.app.worker.monitor; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileWriter; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.SortedSet; import java.util.logging.Level; import java.util.logging.Logger; import org.signalml.app.document.TagDocument; import org.signalml.domain.tag.MonitorTag; import org.signalml.domain.tag.StyledTagSet; import org.signalml.domain.tag.StyledTagSetConverter; import org.signalml.plugin.export.signal.Tag; /** * This class allows to record tags from a {@link MonitorWorker}. To start recording * create a tag recorder and connect it to a {@link MonitorWorker} using {@link MonitorWorker#connectTagRecorderWorker(org.signalml.app.worker.monitor.TagRecorder)}. * To stop recording disconnect it from a the {@link MonitorWorker} using {@link MonitorWorker#disconnectTagRecorderWorker()}. * After disconnecting, the recorded tags can be read using {@link TagRecorder#getRecordedTagSet()}. * * @author Piotr Szachewicz */ public class TagRecorder { /** * The timestamp of the first sample in the samples recording (recorded by the {@link SignalRecorderWorker}). * Used to calculate the position of the tag relatively to the beginning of the recording. */ private double startRecordingTimestamp = Double.NaN; /** * An ArrayList containing the recorded tags. */ private ArrayList<MonitorTag> tagList = new ArrayList<MonitorTag>(); /** * Path to the output file. */ private String filePath; /** * Whether the worker is finished. */ private volatile boolean finished; /** * Ending of the file (everything after last tag). */ private String fileEnding; /** * Length of {@link #fileEnding} in bytes. */ private int endingLength; /** * Default constructor. * @param filePath path to output file */ public TagRecorder(String filePath) { if (!filePath.endsWith(".tag")) { filePath += ".tag"; } this.filePath = filePath; this.finished = false; } /** * Records the given tag. * * @param tag {@link MonitorTag} to be recorded */ public void offerTag(MonitorTag tag) { synchronized (this) { if (!finished) { tagList.add(tag); } } } /** * Saves tags received so far to the output file. */ public void doBackup() { synchronized (this) { doSave(); } } /** * Saves all received tags to the output file. * @param styles styles to be saved */ public void save() { synchronized (this) { doSave(); finished = true; } } /** * Does the saving. * @param tagSet tag set to savetagSet */ public void doSave() { File backingFile = new File(filePath); StyledTagSet tagSet = getRecordedTagSet(); try { // if this is the first backup - create the file normally if (!backingFile.exists()) { TagDocument tagDocument = new TagDocument(tagSet); tagDocument.setBackingFile(backingFile); tagDocument.saveDocument(); findEnding(backingFile); // else - add tags at the end } else { addTags(backingFile, tagSet.getTags()); } removeAllTags(); } catch (Exception ex) { Logger.getLogger(TagRecorder.class.getName()).log(Level.SEVERE, null, ex); } } /** * Finds {@link #fileEnding}. * @param backingFile the file containing the tag document */ private void findEnding(File backingFile) throws FileNotFoundException, IOException { // this is only called once, so we can load the entire file into a single String byte[] buffer = new byte[(int)backingFile.length()]; BufferedInputStream stream = new BufferedInputStream(new FileInputStream(backingFile)); stream.read(buffer); stream.close(); String content = new String(buffer, TagDocument.CHAR_SET); //replace <tags/> with <tags></tags> String previousTagsTag = "<" + StyledTagSetConverter.TAG_NODE_NAME + "/>"; String currentTagsTag = "<" + StyledTagSetConverter.TAG_NODE_NAME + ">" + "</" + StyledTagSetConverter.TAG_NODE_NAME + ">"; content = content.replace(previousTagsTag, currentTagsTag); FileWriter fileWriter = new FileWriter(backingFile); fileWriter.write(content); fileWriter.close(); // closing of the tag section String tagSectionClosing = "</" + StyledTagSetConverter.TAG_NODE_NAME + ">"; // get position of tag section closing, and save everything from that point to fileEnding int start = content.indexOf(tagSectionClosing); fileEnding = content.substring(start); // length of ending in bytes int lengthOfSingleChar = (int)Charset.forName(TagDocument.CHAR_SET).newEncoder().averageBytesPerChar(); endingLength = lengthOfSingleChar * fileEnding.length(); } /** * Adds given tag set to end of tag section of given file. * @param backingFile file to add tags to * @param tags tags to add */ private void addTags(File backingFile, SortedSet<Tag> tags) throws IOException { // get tags to save, and add fileEnding at the end String toSave = StyledTagSetConverter.marshalTagsToString(tags); if (!toSave.isEmpty() && !toSave.endsWith("\n")) { toSave += "\n"; } toSave += fileEnding; // Add tags to the file int bytesToSkip = (int)backingFile.length() - endingLength; RandomAccessFile file = new RandomAccessFile(backingFile, "rwd"); file.skipBytes(bytesToSkip); file.write(toSave.getBytes(TagDocument.CHAR_SET)); file.close(); } /** * Returns the {@link StyledTagSet} containing the tags which were recorded by * this {@link TagRecorder}. * * @return a {@link StyledTagSet} containing the recorded tags */ private StyledTagSet getRecordedTagSet() { if (getStartRecordingTimestamp() == Double.NaN) throw new UnsupportedOperationException("the startRecordingTimestamp was not set for the TagRecorder object"); StyledTagSet styledTagSet = new StyledTagSet(); Tag temporaryTag; for (MonitorTag monitorTag: tagList) { temporaryTag = monitorTag.clone(); temporaryTag.setPosition(monitorTag.getTimestamp() - getStartRecordingTimestamp()); styledTagSet.addTag(temporaryTag); } return styledTagSet; } /** * Should be called to remove all tags after they were saved to a file. */ private void removeAllTags() { tagList.clear(); } /** * Sets the timestamp relatively to which the positions of the recorded tags * will be calculated. * * @param startRecordingTimestamp tags returned by the {@link TagRecorder#getRecordedTagSet()} will * have their positions recalculated relatively to this timestamp. */ public void setStartRecordingTimestamp(double startRecordingTimestamp) { this.startRecordingTimestamp = startRecordingTimestamp; } /** * Returns the timestamp relatively to which the positions of the recorded tags * will be calculated. * * @return the timestamp relatively to which the positions of the tags will * be calculated */ public double getStartRecordingTimestamp() { return startRecordingTimestamp; } /** * Returns if the startRecordingTimestamp was set using {@link TagRecorder#setStartRecordingTimestamp(double)}. * * @return true if the startRecordingTimestamp was set, false otherwise. */ public boolean isStartRecordingTimestampSet() { if (Double.isNaN(startRecordingTimestamp)) return false; return true; } }