blob: 5c8445ba33d644eaff867851e0d926e4eca790ff [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2015, 2016 EfficiOS Inc. and others
*
* 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
*******************************************************************************/
package org.eclipse.tracecompass.internal.provisional.analysis.lami.core.types;
import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;
import java.util.Map;
import java.util.function.Function;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.LamiStrings;
import org.json.JSONException;
import org.json.JSONObject;
import com.google.common.collect.ImmutableMap;
/**
* Base class for data types allowed in LAMI analysis scripts JSON output.
*
* @author Alexandre Montplaisir
* @author Philippe Proulx
*/
public abstract class LamiData {
/**
* Enum of all the valid data types
*/
@SuppressWarnings("javadoc")
public enum DataType {
/* Generic JSON types */
STRING("string", "Value", false, null, LamiString.class), //$NON-NLS-1$ //$NON-NLS-2$
NUMBER("number", "Value", true, null, LamiNumber.class), //$NON-NLS-1$ //$NON-NLS-2$
BOOL("bool", "Value", false, null, LamiBoolean.class), //$NON-NLS-1$ //$NON-NLS-2$
/* Backward-compatibility with pre-1.0 LAMI protocol */
FLOAT("float", "Value", true, null, LamiNumber.class), //$NON-NLS-1$ //$NON-NLS-2$
INTEGER("int", "Value", true, null, LamiNumber.class), //$NON-NLS-1$ //$NON-NLS-2$
/* Lami-specific data types */
RATIO("ratio", "Ratio", true, "%", LamiRatio.class), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
TIMESTAMP("timestamp", "Timestamp", true, "ns", LamiTimestamp.class), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
TIME_RANGE("time-range", "Time range", true, null, LamiTimeRange.class), //$NON-NLS-1$ //$NON-NLS-2$
DURATION("duration", "Duration", true, "ns", LamiDuration.class), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
SIZE("size", "Size", true, Messages.LamiData_UnitBytes, LamiSize.class), //$NON-NLS-1$ //$NON-NLS-2$
BITRATE("bitrate", "Bitrate", true, Messages.LamiData_UnitBitsPerSecond, LamiBitrate.class), //$NON-NLS-1$ //$NON-NLS-2$
SYSCALL("syscall", "System call", false, null, LamiSystemCall.class), //$NON-NLS-1$ //$NON-NLS-2$
PROCESS("process", "Process", false, null, LamiProcess.class), //$NON-NLS-1$ //$NON-NLS-2$
PATH("path", "Path", false, null, LamiPath.class), //$NON-NLS-1$ //$NON-NLS-2$
FD("fd", "File descriptor", false, null, LamiFileDescriptor.class), //$NON-NLS-1$ //$NON-NLS-2$
IRQ("irq", "IRQ", false, null, LamiIRQ.class), //$NON-NLS-1$ //$NON-NLS-2$
CPU("cpu", "CPU", false, null, LamiCPU.class), //$NON-NLS-1$ //$NON-NLS-2$
DISK("disk", "Disk", false, null, LamiDisk.class), //$NON-NLS-1$ //$NON-NLS-2$
PART("part", "Disk partition", false, null, LamiDiskPartition.class), //$NON-NLS-1$ //$NON-NLS-2$
NETIF("netif", "Network interface", false, null, LamiNetworkInterface.class), //$NON-NLS-1$ //$NON-NLS-2$
UNKNOWN("unknown", "Value", false, null, LamiUnknown.class), //$NON-NLS-1$ //$NON-NLS-2$
MIXED("mixed", "Value", false, null, null); //$NON-NLS-1$ //$NON-NLS-2$
private final String fName;
private final String fTitle;
private final boolean fIsContinuous;
private final @Nullable String fUnits;
private final @Nullable Class<?> fClass;
private DataType(String name, String title, boolean isContinous, @Nullable String units, @Nullable Class<?> cls) {
fName = name;
fTitle = title;
fIsContinuous = isContinous;
fUnits = units;
fClass = cls;
}
/**
* Indicates if this data type represents a continuous numerical value.
*
* For example, time or bitrates are continuous values, but CPU or IRQ
* numbers are not (you can't have CPU 1.5!)
*
* @return If this aspect is continuous
*/
public boolean isContinuous() {
return fIsContinuous;
}
/**
* Get the units of this data type, if any.
*
* @return The units, or <code>null</code> if there are no units
*/
public @Nullable String getUnits() {
return fUnits;
}
/**
* The default title for columns containing these units.
*
* @return The data type's column title
*/
public String getTitle() {
return fTitle;
}
/**
* Get the data type from its JSON string representation.
*
* @param value
* The string
* @return The corresponding data type
*/
public static DataType fromString(String value) {
for (DataType type : DataType.values()) {
if (type.fName.equals(value)) {
return type;
}
}
throw new IllegalArgumentException("Unrecognized type: " + value); //$NON-NLS-1$
}
/**
* Get the date type enum element from its implementation Class.
*
* @param cls
* The data type class
* @return The data type
*/
public static @Nullable DataType fromClass(Class<? extends LamiData> cls) {
for (DataType type : DataType.values()) {
if (cls.equals(type.fClass)) {
return type;
}
}
return null;
}
}
@Override
public abstract @Nullable String toString();
// ------------------------------------------------------------------------
// Convenience methods
// ------------------------------------------------------------------------
/**
* Convenience method to get the "name" field from a JSON object. Many LAMI
* types have a "nam" field.
*
* @param obj
* The JSON object
* @return The read name
* @throws JSONException
* If the object does not actually have a "name" field.
*/
private static final String getJSONObjectStringName(JSONObject obj) throws JSONException {
return checkNotNull(obj.getString(LamiStrings.NAME));
}
// ------------------------------------------------------------------------
// "Factory" methods and helpers
// ------------------------------------------------------------------------
/**
* Factory method to build a new LamiData object from either a
* {@link JSONObject} or a standard Java {@link Object} representing a
* primitive type.
*
* @param obj
* The source object
* @return The corresponding LamiData object
* @throws JSONException
* If the object type is not supported
*/
public static LamiData createFromObject(Object obj) throws JSONException {
if (obj instanceof JSONObject) {
return createFromJsonObject((JSONObject) obj);
} else if (obj.equals(JSONObject.NULL)) {
return LamiEmpty.INSTANCE;
} else {
// Backward-compatibility with pre-1.0 LAMI protocol
return createFromPrimitiveObject(obj);
}
}
@FunctionalInterface
private static interface CheckedJSONExceptionFunction<T, R> {
R apply(T t) throws JSONException;
}
@FunctionalInterface
private static interface LamiLongFromValuesFunction {
public LamiLongNumber create(@Nullable Long low, @Nullable Long value, @Nullable Long high);
}
@FunctionalInterface
private static interface LamiDoubleFromValuesFunction {
public LamiDoubleNumber create(@Nullable Double low, @Nullable Double value, @Nullable Double high);
}
/**
* Map returning the Functions to build new LAMI Long Number object from a
* LAMI data object class.
*/
private static final Map<String, LamiLongFromValuesFunction> NUMBER_LONG_TYPE_GENERATOR = ImmutableMap.of(
LamiStrings.DATA_CLASS_TIMESTAMP, (low, value, high) -> new LamiTimestamp(low, value, high),
LamiStrings.DATA_CLASS_DURATION, (low, value, high) -> new LamiDuration(low, value, high),
LamiStrings.DATA_CLASS_SIZE, (low, value, high) -> new LamiSize(low, value, high));
/**
* Map returning the Functions to build new LAMI Double Number object from a
* LAMI data object class.
*/
private static final Map<String, LamiDoubleFromValuesFunction> NUMBER_DOUBLE_TYPE_GENERATOR = ImmutableMap.of(
LamiStrings.DATA_CLASS_NUMBER, (low, value, high) -> new LamiDoubleNumber(low, value, high),
LamiStrings.DATA_CLASS_RATIO, (low, value, high) -> new LamiRatio(low, value, high),
LamiStrings.DATA_CLASS_BITRATE, (low, value, high) -> new LamiBitrate(low, value, high));
/**
* Map returning the Functions to build new LAMI objects for JSON primitive
* types
*/
private static final Map<Class<?>, Function<Object, LamiData>> PRIMITIVE_TYPE_GENERATOR = ImmutableMap.of(
Boolean.class, (o) -> LamiBoolean.instance((Boolean) o),
Integer.class, (o) -> new LamiLongNumber(((Integer) o).longValue()),
Long.class, (o) -> new LamiLongNumber((Long) o),
Double.class, (o) -> new LamiDoubleNumber((Double) o),
String.class, (o) -> new LamiString((String) o));
/**
* Map returning the Functions to build new LAMI objects for LAMI-specific
* types
*/
private static final Map<String, CheckedJSONExceptionFunction<JSONObject, LamiData>> COMPLEX_TYPE_GENERATOR;
static {
ImmutableMap.Builder<String, CheckedJSONExceptionFunction<JSONObject, LamiData>> complexTypeGenBuilder = ImmutableMap.builder();
complexTypeGenBuilder.put(LamiStrings.DATA_CLASS_BITRATE, (obj) -> createFromNumberJsonObject(obj, false));
complexTypeGenBuilder.put(LamiStrings.DATA_CLASS_BOOLEAN, (obj) -> LamiBoolean.instance(obj.getBoolean(LamiStrings.VALUE)));
complexTypeGenBuilder.put(LamiStrings.DATA_CLASS_CPU, (obj) -> new LamiCPU(obj.getInt(LamiStrings.ID)));
complexTypeGenBuilder.put(LamiStrings.DATA_CLASS_DISK, (obj) -> new LamiDisk(getJSONObjectStringName(obj)));
complexTypeGenBuilder.put(LamiStrings.DATA_CLASS_DURATION, (obj) -> createFromNumberJsonObject(obj, true));
complexTypeGenBuilder.put(LamiStrings.DATA_CLASS_PART, (obj) -> new LamiDiskPartition(getJSONObjectStringName(obj)));
complexTypeGenBuilder.put(LamiStrings.DATA_CLASS_FD, (obj) -> new LamiFileDescriptor(obj.getInt(LamiStrings.FD)));
complexTypeGenBuilder.put(LamiStrings.DATA_CLASS_NETIF, (obj) -> new LamiNetworkInterface(getJSONObjectStringName(obj)));
// TODO: Decide whether or not to decode as a long integer
// here instead of forcing decoding as a double. It's possible
// to decode as a long integer when the double value is in
// the range of the long integer, and when the double value
// is an integer (possibly after rounding).
complexTypeGenBuilder.put(LamiStrings.DATA_CLASS_NUMBER, (obj) -> createFromNumberJsonObject(obj, false));
complexTypeGenBuilder.put(LamiStrings.DATA_CLASS_PATH, (obj) -> new LamiPath(checkNotNull(obj.getString(LamiStrings.PATH))));
complexTypeGenBuilder.put(LamiStrings.DATA_CLASS_PROCESS, (obj) -> {
String name = obj.optString(LamiStrings.NAME);
Long pid = (obj.has(LamiStrings.PID) ? obj.getLong(LamiStrings.PID) : null);
Long tid = (obj.has(LamiStrings.TID) ? obj.getLong(LamiStrings.TID) : null);
return new LamiProcess(name, pid, tid);
});
complexTypeGenBuilder.put(LamiStrings.DATA_CLASS_RATIO, (obj) -> createFromNumberJsonObject(obj, false));
complexTypeGenBuilder.put(LamiStrings.DATA_CLASS_IRQ, (obj) -> {
LamiIRQ.Type irqType = LamiIRQ.Type.HARD;
if (obj.has(LamiStrings.HARD)) {
boolean isHardIrq = obj.getBoolean(LamiStrings.HARD);
irqType = (isHardIrq ? LamiIRQ.Type.HARD : LamiIRQ.Type.SOFT);
}
int nr = obj.getInt(LamiStrings.NR);
String name = obj.optString(LamiStrings.NAME);
return new LamiIRQ(irqType, nr, name);
});
complexTypeGenBuilder.put(LamiStrings.DATA_CLASS_SIZE, (obj) -> createFromNumberJsonObject(obj, true));
complexTypeGenBuilder.put(LamiStrings.DATA_CLASS_STRING, (obj) -> new LamiString(obj.getString(LamiStrings.VALUE)));
complexTypeGenBuilder.put(LamiStrings.DATA_CLASS_SYSCALL, (obj) -> new LamiSystemCall(getJSONObjectStringName(obj)));
complexTypeGenBuilder.put(LamiStrings.DATA_CLASS_TIME_RANGE, (obj) -> {
Object beginObj = checkNotNull(obj.get((LamiStrings.BEGIN)));
Object endObj = checkNotNull(obj.get(LamiStrings.END));
LamiTimestamp beginTs;
LamiTimestamp endTs;
if ((beginObj instanceof Long || beginObj instanceof Integer) &&
(endObj instanceof Long || endObj instanceof Integer)) {
Number beginTsNumber = (Number) beginObj;
Number endTsNumber = (Number) endObj;
// Backward-compatibility with pre-1.0 LAMI protocol
beginTs = new LamiTimestamp(beginTsNumber.longValue());
endTs = new LamiTimestamp(endTsNumber.longValue());
} else if (beginObj instanceof JSONObject && endObj instanceof JSONObject) {
// LAMI 1.0
beginTs = (LamiTimestamp) createFromJsonObject((JSONObject) beginObj);
endTs = (LamiTimestamp) createFromJsonObject((JSONObject) endObj);
} else {
throw new JSONException("Invalid time range object"); //$NON-NLS-1$
}
return new LamiTimeRange(beginTs, endTs);
});
complexTypeGenBuilder.put(LamiStrings.DATA_CLASS_TIMESTAMP, (obj) -> createFromNumberJsonObject(obj, true));
complexTypeGenBuilder.put(LamiStrings.DATA_CLASS_UNKNOWN, (obj) -> LamiUnknown.INSTANCE);
COMPLEX_TYPE_GENERATOR = complexTypeGenBuilder.build();
}
/**
* Create a new LamiData for a primitive type (Integer, String, etc.)
*
* @param obj
* The source object
* @return A new corresponding LamiData object
* @throws JSONException
* If the object type is not supported
*/
private static LamiData createFromPrimitiveObject(Object obj) throws JSONException {
Function<Object, LamiData> func = PRIMITIVE_TYPE_GENERATOR.get(obj.getClass());
if (func == null) {
throw new JSONException("Unhandled type: " + obj.toString() + " of type " + obj.getClass().toString()); //$NON-NLS-1$ //$NON-NLS-2$
}
/* We never return null in the implementations */
return checkNotNull(func.apply(obj));
}
/**
* Gets a {@link Number} object from a specific property of a
* {@link JSONObject} object.
*
* @param obj
* JSON object from which to get the number
* @param key
* Key of the property to read
* @param useLong
* {@code true} to decode the number as a long integer
* @return The decoded {@link Number} object
* @throws JSONException If the property is not found
*/
private static @Nullable Number getNumberFromJsonObject(JSONObject obj,
String key, boolean useLong, boolean acceptInfinity) throws JSONException {
Object numberObj = obj.opt(key);
if (numberObj == null) {
return null;
}
if (acceptInfinity && numberObj instanceof String) {
if (numberObj.equals(LamiStrings.NEG_INF)) {
return Double.NEGATIVE_INFINITY;
} else if (numberObj.equals(LamiStrings.POS_INF)) {
return Double.POSITIVE_INFINITY;
}
throw new JSONException("Invalid number: " + numberObj); //$NON-NLS-1$
}
if (useLong) {
return obj.getLong(key);
}
return obj.getDouble(key);
}
private static @Nullable Long nullableNumberToLong(@Nullable Number number) {
return (number == null ? null : Long.valueOf(number.longValue()));
}
private static @Nullable Double nullableNumberToDouble(@Nullable Number number) {
return (number == null ? null : Double.valueOf(number.doubleValue()));
}
/**
* Create a new {@link LamiNumber}-derived type from a {@link JSONObject}.
*
* @param obj
* The JSON object having the LAMI number properties
* @param useLong
* {@code true} to decode the number as a long integer
* @return A new corresponding LamiNumber object
* @throws JSONException
* If the object type is not supported
*/
private static LamiNumber createFromNumberJsonObject(JSONObject obj, boolean useLong) throws JSONException {
String dataClass = obj.optString(LamiStrings.CLASS);
if (dataClass == null) {
throw new JSONException("Cannot find data class"); //$NON-NLS-1$
}
// Get the value, if it's available
Number valueNumber = getNumberFromJsonObject(obj, LamiStrings.VALUE, useLong, false);
// Get the limits, if they're available
Number lowNumber = getNumberFromJsonObject(obj, LamiStrings.LOW, useLong, true);
Number highNumber = getNumberFromJsonObject(obj, LamiStrings.HIGH, useLong, true);
// Validate properties
if (valueNumber == null && (lowNumber == null || highNumber == null)) {
throw new JSONException("Invalid number object: no value, invalid limit"); //$NON-NLS-1$
} else if (lowNumber == null && highNumber != null) {
throw new JSONException("Invalid number object: high limit, but no low limit"); //$NON-NLS-1$
} else if (highNumber == null && lowNumber != null) {
throw new JSONException("Invalid number object: low limit, but no high limit"); //$NON-NLS-1$
}
Number eqLowNumber = lowNumber;
Number eqHighNumber = highNumber;
Number eqValueNumber = valueNumber;
if (eqLowNumber == null) {
eqLowNumber = valueNumber;
eqHighNumber = valueNumber;
} else if (eqValueNumber == null) {
eqValueNumber = eqLowNumber;
}
if (checkNotNull(eqValueNumber).doubleValue() < checkNotNull(eqLowNumber).doubleValue() ||
checkNotNull(eqHighNumber).doubleValue() < checkNotNull(eqValueNumber).doubleValue()) {
throw new JSONException("Invalid number object: low <= value <= high not respected"); //$NON-NLS-1$
}
// Create specific LAMI number object
if (useLong) {
LamiLongFromValuesFunction func = NUMBER_LONG_TYPE_GENERATOR.get(dataClass);
if (func == null) {
throw new JSONException(String.format("Data class \"%s\" is not a number class", dataClass)); //$NON-NLS-1$
}
return func.create(nullableNumberToLong(lowNumber),
nullableNumberToLong(valueNumber),
nullableNumberToLong(highNumber));
}
LamiDoubleFromValuesFunction func = NUMBER_DOUBLE_TYPE_GENERATOR.get(dataClass);
if (func == null) {
throw new JSONException(String.format("Data class \"%s\" is not a number class", dataClass)); //$NON-NLS-1$
}
return func.create(nullableNumberToDouble(lowNumber),
nullableNumberToDouble(valueNumber),
nullableNumberToDouble(highNumber));
}
/**
* Create a new LamiData for a LAMI-specific type from a {@link JSONObject}.
*
* @param obj
* The source object
* @return A new corresponding LamiData object
* @throws JSONException
* If the object type is not supported
*/
private static LamiData createFromJsonObject(JSONObject obj) throws JSONException {
String dataClass = obj.optString(LamiStrings.CLASS);
if (dataClass == null) {
throw new JSONException("Cannot find data class"); //$NON-NLS-1$
}
CheckedJSONExceptionFunction<JSONObject, LamiData> func = COMPLEX_TYPE_GENERATOR.get(dataClass);
if (func == null) {
throw new JSONException(String.format("Unsupported data class \"%s\"", dataClass)); //$NON-NLS-1$
}
return func.apply(obj);
}
}