blob: 58c08a09527fba86c69b2b738fe279a2f3081e99 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2012, 2015 Ericsson
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License 2.0 which
* accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Patrick Tasse - Initial API and implementation
* Marc-Andre Laperle - Add persistent index support
*******************************************************************************/
package org.eclipse.tracecompass.tmf.core.trace.text;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.internal.tmf.core.Activator;
import org.eclipse.tracecompass.tmf.core.event.ITmfEvent;
import org.eclipse.tracecompass.tmf.core.exceptions.TmfTraceException;
import org.eclipse.tracecompass.tmf.core.io.BufferedRandomAccessFile;
import org.eclipse.tracecompass.tmf.core.signal.TmfSignalHandler;
import org.eclipse.tracecompass.tmf.core.signal.TmfTraceRangeUpdatedSignal;
import org.eclipse.tracecompass.tmf.core.timestamp.ITmfTimestamp;
import org.eclipse.tracecompass.tmf.core.timestamp.TmfTimestamp;
import org.eclipse.tracecompass.tmf.core.trace.ITmfContext;
import org.eclipse.tracecompass.tmf.core.trace.TmfTrace;
import org.eclipse.tracecompass.tmf.core.trace.TmfTraceUtils;
import org.eclipse.tracecompass.tmf.core.trace.TraceValidationStatus;
import org.eclipse.tracecompass.tmf.core.trace.indexer.ITmfPersistentlyIndexable;
import org.eclipse.tracecompass.tmf.core.trace.indexer.ITmfTraceIndexer;
import org.eclipse.tracecompass.tmf.core.trace.indexer.TmfBTreeTraceIndexer;
import org.eclipse.tracecompass.tmf.core.trace.indexer.checkpoint.ITmfCheckpoint;
import org.eclipse.tracecompass.tmf.core.trace.indexer.checkpoint.TmfCheckpoint;
import org.eclipse.tracecompass.tmf.core.trace.location.ITmfLocation;
import org.eclipse.tracecompass.tmf.core.trace.location.TmfLongLocation;
/**
* Extension of TmfTrace for handling of line-based text traces parsed using
* regular expressions. Each line that matches the first line pattern indicates
* the start of a new event. The subsequent lines can contain additional
* information that is added to the current event.
*
* @param <T>
* TmfEvent class returned by this trace
*/
public abstract class TextTrace<T extends TextTraceEvent> extends TmfTrace implements ITmfPersistentlyIndexable {
private static final TmfLongLocation NULL_LOCATION = new TmfLongLocation(-1L);
private static final int MAX_LINES = 100;
private static final int MAX_CONFIDENCE = 100;
/** The default separator used for multi-line fields */
protected static final String SEPARATOR = " | "; //$NON-NLS-1$
/** The text file */
protected BufferedRandomAccessFile fFile;
/**
* Constructor
*/
public TextTrace() {
}
/**
* {@inheritDoc}
* <p>
* The default implementation computes the confidence as the sum of weighted
* values of the first 100 lines of the file which match any of the provided
* validation patterns. For each matching line a weighted value between 1.5
* and 2.0 is assigned based on the group count of the matching patterns.
* The higher the group count, the closer the weighted value will be to 2.0.
*/
@Override
public IStatus validate(IProject project, String path) {
File file = new File(path);
if (!file.exists()) {
return new Status(IStatus.ERROR, Activator.PLUGIN_ID, "File not found: " + path); //$NON-NLS-1$
}
if (!file.isFile()) {
return new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Not a file. It's a directory: " + path); //$NON-NLS-1$
}
try {
if (!TmfTraceUtils.isText(file)) {
return new TraceValidationStatus(0, Activator.PLUGIN_ID);
}
} catch (IOException e) {
Activator.logError("Error validating file: " + path, e); //$NON-NLS-1$
return new Status(IStatus.ERROR, Activator.PLUGIN_ID, "IOException validating file: " + path, e); //$NON-NLS-1$
}
int confidence = 0;
try (BufferedRandomAccessFile rafile = new BufferedRandomAccessFile(path, "r")) { //$NON-NLS-1$
int lineCount = 0;
double matches = 0.0;
String line = rafile.getNextLine();
List<Pattern> validationPatterns = getValidationPatterns();
while ((line != null) && (lineCount++ < MAX_LINES)) {
line = preProcessLine(line);
for(Pattern pattern : validationPatterns) {
Matcher matcher = pattern.matcher(line);
if (matcher.matches()) {
int groupCount = matcher.groupCount();
matches += (1.0 + groupCount / ((double) groupCount + 1));
}
}
confidence = (int) (MAX_CONFIDENCE * matches / lineCount);
line = rafile.getNextLine();
}
} catch (IOException e) {
Activator.logError("Error validating file: " + path, e); //$NON-NLS-1$
return new Status(IStatus.ERROR, Activator.PLUGIN_ID, "IOException validating file: " + path, e); //$NON-NLS-1$
}
return new TraceValidationStatus(confidence, Activator.PLUGIN_ID);
}
@Override
public void initTrace(IResource resource, String path, Class<? extends ITmfEvent> type) throws TmfTraceException {
super.initTrace(resource, path, type);
initFile();
}
private void initFile() throws TmfTraceException {
closeFile();
try {
fFile = new BufferedRandomAccessFile(getPath(), "r"); //$NON-NLS-1$
} catch (IOException e) {
throw new TmfTraceException(e.getMessage(), e);
}
}
@Override
public synchronized void dispose() {
super.dispose();
closeFile();
}
private void closeFile() {
if (fFile != null) {
try {
fFile.close();
} catch (IOException e) {
} finally {
fFile = null;
}
}
}
@Override
public synchronized TextTraceContext seekEvent(ITmfLocation location) {
TextTraceContext context = new TextTraceContext(NULL_LOCATION, ITmfContext.UNKNOWN_RANK);
if (NULL_LOCATION.equals(location) || fFile == null) {
return context;
}
try {
if (location == null) {
fFile.seek(0);
} else if (location.getLocationInfo() instanceof Long) {
fFile.seek((Long) location.getLocationInfo());
}
long rawPos = fFile.getFilePointer();
String line = fFile.getNextLine();
while (line != null) {
line = preProcessLine(line);
Matcher matcher = getFirstLinePattern().matcher(line);
if (matcher.matches()) {
setupContext(context, rawPos, line, matcher);
return context;
}
rawPos = fFile.getFilePointer();
line = fFile.getNextLine();
}
return context;
} catch (IOException e) {
Activator.logError("Error seeking file: " + getPath(), e); //$NON-NLS-1$
return context;
}
}
private void setupContext(TextTraceContext context, long rawPos, String line, Matcher matcher) throws IOException {
context.setLocation(new TmfLongLocation(rawPos));
context.firstLineMatcher = matcher;
context.firstLine = line;
context.nextLineLocation = fFile.getFilePointer();
}
@Override
public synchronized TextTraceContext seekEvent(double ratio) {
if (fFile == null) {
return new TextTraceContext(NULL_LOCATION, ITmfContext.UNKNOWN_RANK);
}
try {
long pos = Math.round(ratio * fFile.length());
while (pos > 0) {
fFile.seek(pos - 1);
if (fFile.read() == '\n') {
break;
}
pos--;
}
ITmfLocation location = new TmfLongLocation(Long.valueOf(pos));
TextTraceContext context = seekEvent(location);
context.setRank(ITmfContext.UNKNOWN_RANK);
return context;
} catch (IOException e) {
Activator.logError("Error seeking file: " + getPath(), e); //$NON-NLS-1$
return new TextTraceContext(NULL_LOCATION, ITmfContext.UNKNOWN_RANK);
}
}
@Override
public double getLocationRatio(ITmfLocation location) {
if (fFile == null) {
return 0;
}
try {
long length = fFile.length();
if (length == 0) {
return 0;
}
if (location.getLocationInfo() instanceof Long) {
return (double) ((Long) location.getLocationInfo()) / length;
}
} catch (IOException e) {
Activator.logError("Error reading file: " + getPath(), e); //$NON-NLS-1$
}
return 0;
}
@Override
public ITmfLocation getCurrentLocation() {
return null;
}
@Override
public TextTraceEvent parseEvent(ITmfContext tmfContext) {
TextTraceContext context = seekEvent(tmfContext.getLocation());
return parse(context);
}
@Override
public synchronized @Nullable T getNext(ITmfContext context) {
if (!(context instanceof TextTraceContext)) {
throw new IllegalArgumentException();
}
TextTraceContext savedContext = new TextTraceContext(context.getLocation(), context.getRank());
@Nullable T event = parse((TextTraceContext) context);
if (event != null) {
updateAttributes(savedContext, event);
context.increaseRank();
}
return event;
}
/**
* Parse the next event. The context is advanced.
*
* @param tmfContext
* the context
* @return the next event or null
*/
protected synchronized @Nullable T parse(TextTraceContext tmfContext) {
if (fFile == null) {
return null;
}
TextTraceContext context = tmfContext;
ITmfLocation location = context.getLocation();
if (location == null || !(location.getLocationInfo() instanceof Long) || NULL_LOCATION.equals(location)) {
return null;
}
T event = parseFirstLine(context.firstLineMatcher, context.firstLine);
try {
if (fFile.getFilePointer() != context.nextLineLocation) {
fFile.seek(context.nextLineLocation);
}
long rawPos = fFile.getFilePointer();
String line = fFile.getNextLine();
while (line != null) {
line = preProcessLine(line);
Matcher matcher = getFirstLinePattern().matcher(line);
if (matcher.matches()) {
setupContext(context, rawPos, line, matcher);
return event;
}
parseNextLine(event, line);
rawPos = fFile.getFilePointer();
line = fFile.getNextLine();
}
} catch (IOException e) {
Activator.logError("Error reading file: " + getPath(), e); //$NON-NLS-1$
}
context.setLocation(NULL_LOCATION);
return event;
}
/**
* Pre-processes the input line. The default implementation returns the
* input line.
*
* @param line
* non-null input string
* @return the pre-processed input line
*/
@NonNull
protected String preProcessLine(@NonNull String line) {
return line;
}
/**
* Gets the first line pattern.
*
* @return The first line pattern
*/
protected abstract Pattern getFirstLinePattern();
/**
* Parses the first line data and returns a new event. When constructing the
* event, the concrete trace should use the trace's timestamp transform to
* create the timestamp, by either transforming the parsed time value
* directly or by using the method {@link #createTimestamp(long)}.
*
* @param matcher
* The matcher
* @param line
* The line to parse
* @return The parsed event
*/
protected abstract T parseFirstLine(Matcher matcher, String line);
/**
* Parses the next line data for the current event.
*
* @param event
* The current event being parsed
* @param line
* The line to parse
*/
protected abstract void parseNextLine(T event, String line);
/**
* Returns a ordered list of validation patterns that will be used in
* the default {@link #validate(IProject, String)} method to match
* the first 100 to compute the confidence level
*
* @return collection of patterns to validate against
*/
protected List<Pattern> getValidationPatterns() {
return Collections.singletonList(getFirstLinePattern());
}
// ------------------------------------------------------------------------
// Helper methods
// ------------------------------------------------------------------------
/**
* Strip quotes surrounding a string
*
* @param input
* The input string
* @return The string without quotes
*/
protected static String replaceQuotes(String input) {
String out = input.replaceAll("^\"|(\"\\s*)$", ""); //$NON-NLS-1$//$NON-NLS-2$
return out;
}
/**
* Strip brackets surrounding a string
*
* @param input
* The input string
* @return The string without brackets
*/
protected static String replaceBrackets(String input) {
String out = input.replaceAll("^\\{|(\\}\\s*)$", ""); //$NON-NLS-1$//$NON-NLS-2$
return out;
}
private static int fCheckpointSize = -1;
@Override
public synchronized int getCheckpointSize() {
if (fCheckpointSize == -1) {
TmfCheckpoint c = new TmfCheckpoint(TmfTimestamp.ZERO, new TmfLongLocation(0L), 0);
ByteBuffer b = ByteBuffer.allocate(ITmfCheckpoint.MAX_SERIALIZE_SIZE);
b.clear();
c.serialize(b);
fCheckpointSize = b.position();
}
return fCheckpointSize;
}
@Override
protected ITmfTraceIndexer createIndexer(int interval) {
return new TmfBTreeTraceIndexer(this, interval);
}
@Override
public ITmfLocation restoreLocation(ByteBuffer bufferIn) {
return new TmfLongLocation(bufferIn);
}
@TmfSignalHandler
@Override
public void traceRangeUpdated(TmfTraceRangeUpdatedSignal signal) {
if (signal.getTrace() == this) {
try {
synchronized (this) {
// Reset the file handle in case it has reached the end of the
// file already. Otherwise, it will not be able to read new data
// pass the previous end.
initFile();
}
} catch (TmfTraceException e) {
Activator.logError(e.getLocalizedMessage(), e);
}
}
super.traceRangeUpdated(signal);
}
/**
* @since 3.0
*/
@Override
public synchronized ITmfTimestamp readEnd() {
try {
Long pos = fFile.length() - 1;
/* Outer loop to find the first line of a matcher group. */
while (pos > 0) {
/* Inner loop to find line beginning */
while (pos > 0) {
fFile.seek(pos - 1);
if (fFile.read() == '\n') {
break;
}
pos--;
}
ITmfLocation location = new TmfLongLocation(pos);
ITmfContext context = seekEvent(location);
ITmfEvent event = getNext(context);
context.dispose();
if (event != null) {
return event.getTimestamp();
}
pos--;
}
} catch (IOException e) {
return null;
}
/* Empty trace */
return null;
}
}