blob: 2bd37a6d682e193742ed04863681b7ce2d48fc49 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2014, 2015 École Polytechnique de Montréal
*
* 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:
* Geneviève Bastien - Initial implementation
* Patrick Tasse - Dispose wrapped trace
*******************************************************************************/
package org.eclipse.tracecompass.tmf.tests.stubs.trace.xml;
import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Collection;
import java.util.HashSet;
import java.util.Optional;
import java.util.stream.StreamSupport;
import javax.xml.XMLConstants;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
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.DefaultLocation;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.osgi.util.NLS;
import org.eclipse.tracecompass.internal.tmf.core.Activator;
import org.eclipse.tracecompass.internal.util.ByteBufferTracker;
import org.eclipse.tracecompass.tmf.core.analysis.IAnalysisModule;
import org.eclipse.tracecompass.tmf.core.event.ITmfEvent;
import org.eclipse.tracecompass.tmf.core.event.ITmfEventField;
import org.eclipse.tracecompass.tmf.core.event.ITmfEventType;
import org.eclipse.tracecompass.tmf.core.event.TmfEvent;
import org.eclipse.tracecompass.tmf.core.event.TmfEventField;
import org.eclipse.tracecompass.tmf.core.event.TmfEventType;
import org.eclipse.tracecompass.tmf.core.event.aspect.ITmfEventAspect;
import org.eclipse.tracecompass.tmf.core.event.aspect.TmfBaseAspects;
import org.eclipse.tracecompass.tmf.core.event.aspect.TmfContentFieldAspect;
import org.eclipse.tracecompass.tmf.core.event.aspect.TmfCpuAspect;
import org.eclipse.tracecompass.tmf.core.exceptions.TmfTraceException;
import org.eclipse.tracecompass.tmf.core.parsers.custom.CustomEventContent;
import org.eclipse.tracecompass.tmf.core.parsers.custom.CustomXmlEvent;
import org.eclipse.tracecompass.tmf.core.parsers.custom.CustomXmlTrace;
import org.eclipse.tracecompass.tmf.core.parsers.custom.CustomXmlTraceDefinition;
import org.eclipse.tracecompass.tmf.core.signal.TmfSignalManager;
import org.eclipse.tracecompass.tmf.core.timestamp.ITmfTimestamp;
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.TraceValidationStatus;
import org.eclipse.tracecompass.tmf.core.trace.indexer.ITmfTraceIndexer;
import org.eclipse.tracecompass.tmf.core.trace.indexer.checkpoint.TmfCheckpointIndexer;
import org.eclipse.tracecompass.tmf.core.trace.location.ITmfLocation;
import org.xml.sax.SAXException;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
/**
* An XML development trace using a custom XML trace definition and schema.
*
* This class will typically be used to build custom traces to unit test more
* complex functionalities like analyzes or to develop and test data-driven
* analyzes.
*
* This class wraps a custom XML trace and rewrites the returned events in the
* getNext() method so that event's fields are the ones defined in <field ... />
* elements instead of those defined in the custom XML parser. This way, each
* event can have a different set of fields. This class can, for example, mimic
* a CTF trace.
*
* @author Geneviève Bastien
*/
@SuppressWarnings("restriction")
public abstract class TmfXmlTraceStub extends TmfTrace {
private static final String DEVELOPMENT_TRACE_XSD = "TmfXmlDevelopmentTrace.xsd"; //$NON-NLS-1$
private static final String EMPTY = ""; //$NON-NLS-1$
/* XML elements and attributes names */
private static final String EVENT_NAME_FIELD = "Message"; //$NON-NLS-1$
private static final String FIELD_NAMES_FIELD = "fields"; //$NON-NLS-1$
private static final String VALUES_FIELD = "values"; //$NON-NLS-1$
private static final String TYPES_FIELD = "type"; //$NON-NLS-1$
private static final String VALUES_SEPARATOR = " \\| "; //$NON-NLS-1$
private static final String TYPE_INTEGER = "int"; //$NON-NLS-1$
private static final String TYPE_LONG = "long"; //$NON-NLS-1$
private static final String TYPE_LONG_ARRAY = "longArray"; //$NON-NLS-1$
private static final String TYPE_INT_ARRAY = "intArray"; //$NON-NLS-1$
private static final String ASPECT_CPU = "cpu";
private final CustomXmlTraceDefinition fDefinition;
private CustomXmlTrace fTrace;
private Collection<ITmfEventAspect<?>> fAspects = TmfTrace.BASE_ASPECTS;
private final Collection<ITmfEventAspect<?>> fAdditionalAspects = new HashSet<>();
private final Collection<IAnalysisModule> fAdditionalModules = new HashSet<>();
/**
* Constructor. Constructs the custom XML trace with the appropriate
* definition.
*/
public TmfXmlTraceStub() {
boolean nonNullTraceIsNonNull = false;
/* Load custom XML definition */
try (InputStream in = TmfXmlTraceStub.class.getResourceAsStream(getParserFileName());) {
CustomXmlTraceDefinition[] definitions = CustomXmlTraceDefinition.loadAll(in);
if (definitions.length < 2) {
throw new IllegalStateException("The custom trace definition does not exist"); //$NON-NLS-1$
}
/* The first definition parses the 'set_aspects' event */
fTrace = new CustomXmlTrace(definitions[0]) {
@Override
protected ITmfTraceIndexer createIndexer(int interval) {
/* Use the in-memory checkpoint indexer */
return new TmfCheckpointIndexer(this, interval);
}
};
TmfSignalManager.deregister(fTrace);
nonNullTraceIsNonNull = true;
/* The second definition parses 'event' trace events */
fDefinition = checkNotNull(definitions[1]);
} catch (IOException e) {
if (nonNullTraceIsNonNull) {
fTrace.dispose();
}
throw new IllegalStateException("Cannot open the trace parser for development traces"); //$NON-NLS-1$
}
}
/**
* Get the name of the file that contains the XML parser for this trace
* stub. The file should be located in this class's package.
*
* @return The name of the file
*/
protected abstract String getParserFileName();
@Override
@NonNullByDefault({DefaultLocation.TYPE_ARGUMENT})
public void initTrace(@Nullable IResource resource, @Nullable String path, @Nullable Class<? extends ITmfEvent> type) throws TmfTraceException {
super.initTrace(resource, path, type);
ITmfContext ctx;
/* Initialize and read the trace with the 'set_aspects' definition */
TmfSignalManager.deregister(fTrace);
fTrace.initTrace(resource, path, type);
ctx = seekEvent(0L);
/* If a set_aspects event exists, getNext() will process it */
getNext(ctx);
ctx.dispose();
fTrace.dispose();
/* Initialize a new trace with the trace events definition */
fTrace = new CustomXmlTrace(fDefinition);
TmfSignalManager.deregister(fTrace);
fTrace.initTrace(resource, path, type);
/* Set the start and (current) end times for this trace */
ctx = seekEvent(0L);
if (ctx == null) {
return;
}
ITmfEvent event = getNext(ctx);
if (event != null) {
final ITmfTimestamp curTime = event.getTimestamp();
this.setStartTime(curTime);
this.setEndTime(curTime);
}
ctx.dispose();
}
@Override
public synchronized void dispose() {
super.dispose();
fTrace.dispose();
}
@Override
public @Nullable ITmfEvent parseEvent(@Nullable ITmfContext context) {
return fTrace.parseEvent(context);
}
@Override
public @Nullable ITmfLocation getCurrentLocation() {
return fTrace.getCurrentLocation();
}
@Override
public double getLocationRatio(@Nullable ITmfLocation location) {
return fTrace.getLocationRatio(location);
}
@Override
public @Nullable ITmfContext seekEvent(@Nullable ITmfLocation location) {
return fTrace.seekEvent(location);
}
@Override
public @Nullable ITmfContext seekEvent(double ratio) {
return fTrace.seekEvent(ratio);
}
@Override
public IStatus validate(@Nullable IProject project, @Nullable String path) {
File xmlFile = new File(path);
if (!xmlFile.exists() || !xmlFile.isFile() || !xmlFile.canRead()) {
return new Status(IStatus.ERROR, Activator.PLUGIN_ID, NLS.bind(org.eclipse.tracecompass.tmf.tests.stubs.trace.xml.Messages.TmfDevelopmentTrace_FileNotFound, path));
}
/* Does the XML file validate with the XSD */
SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
Source xmlSource = new StreamSource(xmlFile);
try {
URL url = TmfXmlTraceStub.class.getResource(DEVELOPMENT_TRACE_XSD);
Schema schema = schemaFactory.newSchema(url);
Validator validator = schema.newValidator();
validator.validate(xmlSource);
} catch (SAXException e) {
return new Status(IStatus.ERROR, Activator.PLUGIN_ID, NLS.bind(org.eclipse.tracecompass.tmf.tests.stubs.trace.xml.Messages.TmfDevelopmentTrace_ValidationError, path), e);
} catch (IOException e) {
return new Status(IStatus.ERROR, Activator.PLUGIN_ID, NLS.bind(org.eclipse.tracecompass.tmf.tests.stubs.trace.xml.Messages.TmfDevelopmentTrace_IoError, path), e);
} finally {
/**
* The Validator keeps a reference to the file resource when it fails. In Windows, this reference will
* prevent the deletion of the file. Running a GC will remove the reference. Use the ByteBufferTracker
* to trigger a GC before the deletion of a trace element.
*/
ByteBufferTracker.setMarked();
}
// XML validates, we have confidence it is this trace type
IStatus status = new TraceValidationStatus(1, Activator.PLUGIN_ID);
return status;
}
private static String getStringValue(ITmfEventField content, String fieldName) {
ITmfEventField field = content.getField(fieldName);
if (field == null) {
return EMPTY;
}
Object val = field.getValue();
if (!(val instanceof String)) {
return EMPTY;
}
return (String) val;
}
@Override
public synchronized @Nullable ITmfEvent getNext(@Nullable ITmfContext context) {
if (context == null) {
return null;
}
final ITmfContext savedContext = new TmfContext(context.getLocation(), context.getRank());
CustomXmlEvent event = fTrace.getNext(context);
if (event == null) {
return null;
}
/* Translate the content of the event */
/* The "fields" field contains a | separated list of field names */
/* The "values" field contains a | separated list of field values */
/* the "type" field contains a | separated list of field types */
ITmfEventField content = event.getContent();
if (content == null) {
return null;
}
String fieldString = getStringValue(content, FIELD_NAMES_FIELD);
String valueString = getStringValue(content, VALUES_FIELD);
String typeString = getStringValue(content, TYPES_FIELD);
String[] fields = fieldString.split(VALUES_SEPARATOR);
String[] values = valueString.split(VALUES_SEPARATOR);
String[] types = typeString.split(VALUES_SEPARATOR);
ITmfEventField[] fieldsArray = new TmfEventField[fields.length];
for (int i = 0; i < fields.length; i++) {
String value = EMPTY;
if (values.length > i) {
value = values[i];
}
String type = null;
if (types.length > i) {
type = types[i];
}
Object val = value;
if (type != null) {
switch (type) {
case TYPE_INTEGER: {
try {
val = Integer.valueOf(value);
} catch (NumberFormatException e) {
Activator.logError(String.format("Get next XML event: cannot cast value %s to integer", value), e); //$NON-NLS-1$
val = 0;
}
break;
}
case TYPE_LONG: {
try {
val = Long.valueOf(value);
} catch (NumberFormatException e) {
Activator.logError(String.format("Get next XML event: cannot cast value %s to long", value), e); //$NON-NLS-1$
val = 0L;
}
break;
}
case TYPE_LONG_ARRAY: {
try {
String[] split = value.split(",");
long[] arr = new long[split.length];
for (int j = 0; j < split.length; j++) {
arr[j] = Long.valueOf(split[j]);
}
val = arr;
} catch (NumberFormatException e) {
Activator.logError(String.format("Get next XML event: cannot cast one of the comma-separated values of %s to long", value), e); //$NON-NLS-1$
val = new long[0];
}
break;
}
case TYPE_INT_ARRAY: {
try {
String[] split = value.split(",");
int[] arr = new int[split.length];
for (int j = 0; j < split.length; j++) {
arr[j] = Integer.valueOf(split[j]);
}
val = arr;
} catch (NumberFormatException e) {
Activator.logError(String.format("Get next XML event: cannot cast one of the comma-separated values of %s to int", value), e); //$NON-NLS-1$
val = new int[0];
}
break;
}
default:
break;
}
}
fieldsArray[i] = new TmfEventField(checkNotNull(fields[i]), val, null);
}
/*
* Generate the aspects for this trace if it is the 'set_aspects'
* definition
*/
if (fTrace.getDefinition() != fDefinition) {
generateAspects(fieldsArray);
return null;
}
/* Create a new event with new fields and name */
ITmfEventType customEventType = event.getType();
String eventName = getStringValue(content, EVENT_NAME_FIELD);
TmfEventType eventType = new TmfEventType(eventName, customEventType.getRootField());
ITmfEventField eventFields = new CustomEventContent(content.getName(), content.getValue(), fieldsArray);
/*
* TODO: Timestamps for these traces are in nanos, but since the
* CustomXmlTrace does not support this format, the timestamp of the
* original is in second and we need to convert it. We should do that at
* the source when it is supported
*/
TmfEvent newEvent = new TmfEvent(this, ITmfContext.UNKNOWN_RANK, event.getTimestamp(), eventType, eventFields);
updateAttributes(savedContext, event);
return newEvent;
}
private void generateAspects(ITmfEventField[] fieldsArray) {
ImmutableList.Builder<ITmfEventAspect<?>> builder = new ImmutableList.Builder<>();
/* Initialize the first default trace aspects */
builder.add(TmfBaseAspects.getTimestampAspect());
builder.add(TmfBaseAspects.getEventTypeAspect());
/* Add custom aspects in between */
for (ITmfEventField field : fieldsArray) {
String name = field.getName();
final ITmfEventAspect<?> aspect = new TmfContentFieldAspect(name, name);
if (name.equals(ASPECT_CPU)) {
builder.add(new TmfCpuAspect() {
@Override
public @Nullable Integer resolve(ITmfEvent event) {
Object result = aspect.resolve(event);
if (result instanceof Number) {
return ((Number) result).intValue();
}
return null;
}
});
} else {
builder.add(aspect);
}
}
/* Add the big content aspect */
builder.add(TmfBaseAspects.getContentsAspect());
/* Add the additional aspects */
builder.addAll(fAdditionalAspects);
fAspects = builder.build();
}
@Override
public Iterable<ITmfEventAspect<?>> getEventAspects() {
return fAspects;
}
/**
* Adds a new event aspect to this type of trace. Since this trace type is
* used to build custom traces that mimic the behavior of real traces, the
* required aspects may be missing and this method allows to add them.
*
* @param aspect
* The aspect to have
*/
public void addEventAspect(ITmfEventAspect<?> aspect) {
fAdditionalAspects.add(aspect);
ImmutableSet.Builder<ITmfEventAspect<?>> builder = new ImmutableSet.Builder<>();
builder.addAll(fAspects);
builder.add(aspect);
fAspects = builder.build();
}
/**
* Add an additional new module
*
* @param module
* The new module
*/
public void addAnalysisModule(IAnalysisModule module) {
fAdditionalModules.add(module);
}
@Override
public Iterable<@NonNull IAnalysisModule> getAnalysisModules() {
@NonNull Iterable<IAnalysisModule> modules = super.getAnalysisModules();
return checkNotNull(Iterables.concat(modules, fAdditionalModules));
}
@Override
public @Nullable IAnalysisModule getAnalysisModule(@Nullable String analysisId) {
Iterable<@NonNull IAnalysisModule> modules = getAnalysisModules();
Optional<IAnalysisModule> opt = StreamSupport.stream(modules.spliterator(), false).filter(analysis -> analysis.getId().equals(analysisId)).findFirst();
return opt.isPresent() ? opt.get() : null;
}
}