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