blob: 7e1f1d87165532202d29c6985df20ac2e9c461a4 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2010, 2016 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
* Bernd Hufmann - Add trace type id handling
*******************************************************************************/
package org.eclipse.tracecompass.tmf.core.parsers.custom;
import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.regex.Matcher;
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.tracecompass.internal.tmf.core.Activator;
import org.eclipse.tracecompass.internal.tmf.core.parsers.custom.CustomEventAspects;
import org.eclipse.tracecompass.tmf.core.event.ITmfEvent;
import org.eclipse.tracecompass.tmf.core.event.ITmfEventField;
import org.eclipse.tracecompass.tmf.core.event.aspect.ITmfEventAspect;
import org.eclipse.tracecompass.tmf.core.exceptions.TmfTraceException;
import org.eclipse.tracecompass.tmf.core.io.BufferedRandomAccessFile;
import org.eclipse.tracecompass.tmf.core.parsers.custom.CustomTxtTraceDefinition.InputLine;
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.TmfContext;
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;
/**
* Base class for custom plain text traces.
*
* @author Patrick Tassé
*/
public class CustomTxtTrace extends TmfTrace implements ITmfPersistentlyIndexable {
private static final TmfLongLocation NULL_LOCATION = new TmfLongLocation(-1L);
private static final int DEFAULT_CACHE_SIZE = 100;
private static final int MAX_LINES = 100;
private static final int MAX_CONFIDENCE = 100;
private final CustomTxtTraceDefinition fDefinition;
private final ITmfEventField fRootField;
private BufferedRandomAccessFile fFile;
private final @NonNull String fTraceTypeId;
private static final char SEPARATOR = ':';
private static final String CUSTOM_TXT_TRACE_TYPE_PREFIX = "custom.txt.trace" + SEPARATOR; //$NON-NLS-1$
private static final String LINUX_TOOLS_CUSTOM_TXT_TRACE_TYPE_PREFIX = "org.eclipse.linuxtools.tmf.core.parsers.custom.CustomTxtTrace" + SEPARATOR; //$NON-NLS-1$
private static final String EARLY_TRACE_COMPASS_CUSTOM_TXT_TRACE_TYPE_PREFIX = "org.eclipse.tracecompass.tmf.core.parsers.custom.CustomTxtTrace" + SEPARATOR; //$NON-NLS-1$
/**
* Basic constructor.
*
* @param definition
* Text trace definition
*/
public CustomTxtTrace(final CustomTxtTraceDefinition definition) {
fDefinition = definition;
fRootField = CustomEventType.getRootField(definition);
fTraceTypeId = buildTraceTypeId(definition.categoryName, definition.definitionName);
setCacheSize(DEFAULT_CACHE_SIZE);
}
/**
* Full constructor.
*
* @param resource
* Trace's resource.
* @param definition
* Text trace definition
* @param path
* Path to the trace file
* @param cacheSize
* Cache size to use
* @throws TmfTraceException
* If we couldn't open the trace at 'path'
*/
public CustomTxtTrace(final IResource resource,
final CustomTxtTraceDefinition definition, final String path,
final int cacheSize) throws TmfTraceException {
this(definition);
setCacheSize((cacheSize > 0) ? cacheSize : DEFAULT_CACHE_SIZE);
initTrace(resource, path, CustomTxtEvent.class);
}
@Override
public void initTrace(final IResource resource, final String path, final Class<? extends ITmfEvent> eventType) throws TmfTraceException {
super.initTrace(resource, path, eventType);
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 ITmfTraceIndexer getIndexer() {
return super.getIndexer();
}
@Override
public Iterable<ITmfEventAspect<?>> getEventAspects() {
return CustomEventAspects.generateAspects(fDefinition);
}
@Override
public synchronized TmfContext seekEvent(final ITmfLocation location) {
final CustomTxtTraceContext context = new CustomTxtTraceContext(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) {
for (final InputLine input : getFirstLines()) {
final Matcher matcher = input.getPattern().matcher(line);
if (matcher.matches()) {
context.setLocation(new TmfLongLocation(rawPos));
context.firstLineMatcher = matcher;
context.firstLine = line;
context.nextLineLocation = fFile.getFilePointer();
context.inputLine = input;
return context;
}
}
rawPos = fFile.getFilePointer();
line = fFile.getNextLine();
}
return context;
} catch (final FileNotFoundException e) {
Activator.logError("Error seeking event. File not found: " + getPath(), e); //$NON-NLS-1$
return context;
} catch (final IOException e) {
Activator.logError("Error seeking event. File: " + getPath(), e); //$NON-NLS-1$
return context;
}
}
@Override
public synchronized TmfContext seekEvent(final double ratio) {
if (fFile == null) {
return new CustomTxtTraceContext(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--;
}
final ITmfLocation location = new TmfLongLocation(pos);
final TmfContext context = seekEvent(location);
context.setRank(ITmfContext.UNKNOWN_RANK);
return context;
} catch (final IOException e) {
Activator.logError("Error seeking event. File: " + getPath(), e); //$NON-NLS-1$
return new CustomTxtTraceContext(NULL_LOCATION, ITmfContext.UNKNOWN_RANK);
}
}
@Override
public synchronized double getLocationRatio(final ITmfLocation location) {
if (fFile == null) {
return 0;
}
try {
if (location.getLocationInfo() instanceof Long) {
return ((Long) location.getLocationInfo()).doubleValue() / fFile.length();
}
} catch (final IOException e) {
Activator.logError("Error seeking event. File: " + getPath(), e); //$NON-NLS-1$
}
return 0;
}
@Override
public ITmfLocation getCurrentLocation() {
// TODO Auto-generated method stub
return null;
}
@Override
public synchronized CustomTxtEvent parseEvent(final ITmfContext tmfContext) {
ITmfContext context = seekEvent(tmfContext.getLocation());
return parse(context);
}
@Override
public synchronized CustomTxtEvent getNext(final ITmfContext context) {
final ITmfContext savedContext = new TmfContext(context.getLocation(), context.getRank());
final CustomTxtEvent event = parse(context);
if (event != null) {
updateAttributes(savedContext, event);
context.increaseRank();
}
return event;
}
private synchronized CustomTxtEvent parse(final ITmfContext tmfContext) {
if (fFile == null) {
return null;
}
if (!(tmfContext instanceof CustomTxtTraceContext)) {
return null;
}
final CustomTxtTraceContext context = (CustomTxtTraceContext) tmfContext;
ITmfLocation location = context.getLocation();
if (location == null || !(location.getLocationInfo() instanceof Long) || NULL_LOCATION.equals(location)) {
return null;
}
CustomTxtEvent event = parseFirstLine(context);
final HashMap<InputLine, Integer> countMap = new HashMap<>();
InputLine currentInput = null;
if (context.inputLine.childrenInputs != null && !context.inputLine.childrenInputs.isEmpty()) {
currentInput = context.inputLine.childrenInputs.get(0);
countMap.put(currentInput, 0);
}
try {
if (fFile.getFilePointer() != context.nextLineLocation) {
fFile.seek(context.nextLineLocation);
}
long rawPos = fFile.getFilePointer();
String line = fFile.getNextLine();
while (line != null) {
boolean processed = false;
if (currentInput == null) {
for (final InputLine input : getFirstLines()) {
final Matcher matcher = input.getPattern().matcher(line);
if (matcher.matches()) {
context.setLocation(new TmfLongLocation(rawPos));
context.firstLineMatcher = matcher;
context.firstLine = line;
context.nextLineLocation = fFile.getFilePointer();
context.inputLine = input;
return event;
}
}
} else {
if (checkNotNull(countMap.get(currentInput)) >= currentInput.getMinCount()) {
final List<InputLine> nextInputs = currentInput.getNextInputs(countMap);
if (nextInputs.isEmpty() || nextInputs.get(nextInputs.size() - 1).getMinCount() == 0) {
for (final InputLine input : getFirstLines()) {
final Matcher matcher = input.getPattern().matcher(line);
if (matcher.matches()) {
context.setLocation(new TmfLongLocation(rawPos));
context.firstLineMatcher = matcher;
context.firstLine = line;
context.nextLineLocation = fFile.getFilePointer();
context.inputLine = input;
return event;
}
}
}
for (final InputLine input : nextInputs) {
final Matcher matcher = input.getPattern().matcher(line);
if (matcher.matches()) {
event.processGroups(input, matcher);
currentInput = input;
if (countMap.get(currentInput) == null) {
countMap.put(currentInput, 1);
} else {
countMap.put(currentInput, checkNotNull(countMap.get(currentInput)) + 1);
}
Iterator<InputLine> iter = countMap.keySet().iterator();
while (iter.hasNext()) {
final InputLine inputLine = iter.next();
if (inputLine.level > currentInput.level) {
iter.remove();
}
}
if (currentInput.childrenInputs != null && !currentInput.childrenInputs.isEmpty()) {
currentInput = currentInput.childrenInputs.get(0);
countMap.put(currentInput, 0);
} else if (checkNotNull(countMap.get(currentInput)) >= currentInput.getMaxCount()) {
if (!currentInput.getNextInputs(countMap).isEmpty()) {
currentInput = currentInput.getNextInputs(countMap).get(0);
if (countMap.get(currentInput) == null) {
countMap.put(currentInput, 0);
}
iter = countMap.keySet().iterator();
while (iter.hasNext()) {
final InputLine inputLine = iter.next();
if (inputLine.level > currentInput.level) {
iter.remove();
}
}
} else {
currentInput = null;
}
}
processed = true;
break;
}
}
}
if (!processed && currentInput != null) {
final Matcher matcher = currentInput.getPattern().matcher(line);
if (matcher.matches()) {
event.processGroups(currentInput, matcher);
countMap.put(currentInput, checkNotNull(countMap.get(currentInput)) + 1);
if (currentInput.childrenInputs != null && !currentInput.childrenInputs.isEmpty()) {
currentInput = currentInput.childrenInputs.get(0);
countMap.put(currentInput, 0);
} else if (checkNotNull(countMap.get(currentInput)) >= currentInput.getMaxCount()) {
if (!currentInput.getNextInputs(countMap).isEmpty()) {
currentInput = currentInput.getNextInputs(countMap).get(0);
if (countMap.get(currentInput) == null) {
countMap.put(currentInput, 0);
}
final Iterator<InputLine> iter = countMap.keySet().iterator();
while (iter.hasNext()) {
final InputLine inputLine = iter.next();
if (inputLine.level > currentInput.level) {
iter.remove();
}
}
} else {
currentInput = null;
}
}
}
((StringBuffer) event.getContentValue()).append("\n").append(line); //$NON-NLS-1$
}
}
rawPos = fFile.getFilePointer();
line = fFile.getNextLine();
}
} catch (final IOException e) {
Activator.logError("Error seeking event. File: " + getPath(), e); //$NON-NLS-1$
}
for (final Entry<InputLine, Integer> entry : countMap.entrySet()) {
if (entry.getValue() < entry.getKey().getMinCount()) {
event = null;
}
}
context.setLocation(NULL_LOCATION);
return event;
}
/**
* @return The first few lines of the text file
*/
public List<InputLine> getFirstLines() {
return fDefinition.inputs;
}
/**
* Parse the first line of the trace (to recognize the type).
*
* @param context
* Trace context
* @return The first event
*/
public CustomTxtEvent parseFirstLine(final CustomTxtTraceContext context) {
CustomTxtEventType eventType = new CustomTxtEventType(checkNotNull(fDefinition.definitionName), fRootField);
final CustomTxtEvent event = new CustomTxtEvent(fDefinition, this, TmfTimestamp.ZERO, eventType);
event.processGroups(context.inputLine, context.firstLineMatcher);
event.setContent(new CustomEventContent(event, new StringBuffer(context.firstLine)));
return event;
}
/**
* Get the trace definition.
*
* @return The trace definition
*/
public CustomTraceDefinition getDefinition() {
return fDefinition;
}
/**
* {@inheritDoc}
* <p>
* The default implementation computes the confidence as the percentage of
* lines in the first 100 lines of the file which match any of the root
* input line patterns.
*/
@Override
public IStatus validate(IProject project, String path) {
File file = new File(path);
if (!file.exists() || !file.isFile() || !file.canRead()) {
return new Status(IStatus.ERROR, Activator.PLUGIN_ID, Messages.CustomTrace_FileNotFound + ": " + path); //$NON-NLS-1$
}
int confidence = 0;
try {
if (!TmfTraceUtils.isText(file)) {
return new TraceValidationStatus(confidence, 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$
}
try (BufferedRandomAccessFile rafile = new BufferedRandomAccessFile(path, "r")) { //$NON-NLS-1$
int lineCount = 0;
double matches = 0.0;
String line = rafile.getNextLine();
while ((line != null) && (lineCount++ < MAX_LINES)) {
for (InputLine inputLine : fDefinition.inputs) {
Matcher matcher = inputLine.getPattern().matcher(line);
if (matcher.matches()) {
int groupCount = matcher.groupCount();
matches += (1.0 + groupCount / ((double) groupCount + 1));
break;
}
}
confidence = (int) (MAX_CONFIDENCE * matches / lineCount);
line = rafile.getNextLine();
}
} catch (IOException e) {
return new Status(IStatus.ERROR, Activator.PLUGIN_ID, "IOException validating file: " + path, e); //$NON-NLS-1$
}
return new TraceValidationStatus(confidence, Activator.PLUGIN_ID);
}
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
public ITmfLocation restoreLocation(ByteBuffer bufferIn) {
return new TmfLongLocation(bufferIn);
}
@Override
protected ITmfTraceIndexer createIndexer(int interval) {
return new TmfBTreeTraceIndexer(this, interval);
}
@Override
public String getTraceTypeId() {
return fTraceTypeId;
}
/**
* Build the trace type id for a custom text trace
*
* @param category
* the category
* @param definitionName
* the definition name
* @return the trace type id
*/
public static @NonNull String buildTraceTypeId(String category, String definitionName) {
return CUSTOM_TXT_TRACE_TYPE_PREFIX + category + SEPARATOR + definitionName;
}
/**
* Checks whether the given trace type ID is a custom text trace type ID
*
* @param traceTypeId
* the trace type ID to check
* @return <code>true</code> if it's a custom text trace type ID else <code>false</code>
*/
public static boolean isCustomTraceTypeId(@NonNull String traceTypeId) {
return traceTypeId.startsWith(CUSTOM_TXT_TRACE_TYPE_PREFIX);
}
/**
* This methods builds a trace type ID from a given ID taking into
* consideration any format changes that were done for the IDs of custom
* text traces. For example, such format change took place when moving to
* Trace Compass. Trace type IDs that are part of the plug-in extension for
* trace types won't be changed.
*
* This method is useful for IDs that were persisted in the workspace before
* the format changes (e.g. in the persistent properties of a trace
* resource).
*
* It ensures backwards compatibility of the workspace for custom text
* traces.
*
* @param traceTypeId
* the legacy trace type ID
* @return the trace type id in Trace Compass format
*/
public static @NonNull String buildCompatibilityTraceTypeId(@NonNull String traceTypeId) {
// Handle early Trace Compass custom text trace type IDs
if (traceTypeId.startsWith(EARLY_TRACE_COMPASS_CUSTOM_TXT_TRACE_TYPE_PREFIX)) {
return CUSTOM_TXT_TRACE_TYPE_PREFIX + traceTypeId.substring(EARLY_TRACE_COMPASS_CUSTOM_TXT_TRACE_TYPE_PREFIX.length());
}
// Handle Linux Tools custom text trace type IDs (with and without category)
int index = traceTypeId.lastIndexOf(SEPARATOR);
if ((index != -1) && (traceTypeId.startsWith(LINUX_TOOLS_CUSTOM_TXT_TRACE_TYPE_PREFIX))) {
String definitionName = index < traceTypeId.length() ? traceTypeId.substring(index + 1) : ""; //$NON-NLS-1$
if (traceTypeId.contains(CustomTxtTrace.class.getSimpleName() + SEPARATOR) && traceTypeId.indexOf(SEPARATOR) == index) {
return buildTraceTypeId(CustomTxtTraceDefinition.CUSTOM_TXT_CATEGORY, definitionName);
}
return CUSTOM_TXT_TRACE_TYPE_PREFIX + traceTypeId.substring(LINUX_TOOLS_CUSTOM_TXT_TRACE_TYPE_PREFIX.length());
}
return traceTypeId;
}
@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 = parseEvent(context);
context.dispose();
if (event != null) {
/* The last event in the trace was successfully parsed. */
return event.getTimestamp();
}
/* pos was after the beginning of the lines of the last event. */
pos--;
}
} catch (IOException e) {
Activator.logError("Error seeking last event. File: " + getPath(), e); //$NON-NLS-1$
}
/* Empty trace */
return null;
}
}