/******************************************************************************* * 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 v1.0 which * accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * 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.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); } }