From e5a027d57edeeab980225b82b8c533d3fe380536 Mon Sep 17 00:00:00 2001 From: Armin Date: Mon, 24 Jun 2024 13:21:00 +0200 Subject: [PATCH] Convert MeasurementIdentifier to Kotlin --- .../cyface/model/MeasurementIdentifier.java | 142 ------- .../de/cyface/model/MeasurementIdentifier.kt | 134 +++++++ .../kotlin/de/cyface/model/RequestMetaData.kt | 356 +++++++++++++----- 3 files changed, 387 insertions(+), 245 deletions(-) delete mode 100644 libs/model/src/main/java/de/cyface/model/MeasurementIdentifier.java create mode 100644 libs/model/src/main/kotlin/de/cyface/model/MeasurementIdentifier.kt diff --git a/libs/model/src/main/java/de/cyface/model/MeasurementIdentifier.java b/libs/model/src/main/java/de/cyface/model/MeasurementIdentifier.java deleted file mode 100644 index 5dd05d9..0000000 --- a/libs/model/src/main/java/de/cyface/model/MeasurementIdentifier.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright 2021 Cyface GmbH - * - * This file is part of the Serialization. - * - * The Serialization is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * The Serialization is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with the Serialization. If not, see . - */ -package de.cyface.model; - -import org.apache.commons.lang3.Validate; - -import java.io.Serializable; - -/** - * Describes the world wide unique identifier of a measurement. This identifier consists of a device identifier and the - * actual measurement from that device. - *

- * The natural order of MeasurementIdentifier is lexicographically by device identifier and then by - * measurement identifier. - * - * @author Klemens Muthmann - * @version 1.0.0 - * @since 2.0.0 - */ -public final class MeasurementIdentifier implements Comparable, Serializable { - - /** - * Used to serialize objects of this class. Only change this value if this classes attribute set changes. - */ - private static final long serialVersionUID = 181303400330020850L; - /** - * The world wide unique identifier of the device that captured this measurement. - */ - private String deviceIdentifier; - /** - * The device wide unique identifier of the measurement. - */ - private long measurementIdentifier; - - /** - * The default no arguments constructor as required by Apache Flink to serialize and deserialize objects of this - * class. Do not use this in you own code, it creates an unusable MeasurementIdentifier. - */ - public MeasurementIdentifier() { - // Nothing to do here. - } - - /** - * Creates a new completely initialized object of this class. - * - * @param deviceIdentifier The world wide unique identifier of the device that captured this measurement - * @param measurementIdentifier The device wide unique identifier of the measurement - */ - public MeasurementIdentifier(final String deviceIdentifier, final long measurementIdentifier) { - setDeviceIdentifier(deviceIdentifier); - setMeasurementIdentifier(measurementIdentifier); - } - - /** - * @return The world wide unique identifier of the device that captured this measurement - */ - public String getDeviceIdentifier() { - return deviceIdentifier; - } - - /** - * @return The device wide unique identifier of the measurement - */ - public long getMeasurementIdentifier() { - return measurementIdentifier; - } - - /** - * @param deviceIdentifier The world wide unique identifier of the device that captured this measurement - */ - public void setDeviceIdentifier(final String deviceIdentifier) { - Validate.notEmpty(deviceIdentifier); - - this.deviceIdentifier = deviceIdentifier; - } - - /** - * @param measurementIdentifier The device wide unique identifier of the measurement - */ - public void setMeasurementIdentifier(final long measurementIdentifier) { - Validate.isTrue(measurementIdentifier >= 0); - - this.measurementIdentifier = measurementIdentifier; - } - - @Override - public int hashCode() { - final var prime = 31; - int result = 1; - result = prime * result + ((deviceIdentifier == null) ? 0 : deviceIdentifier.hashCode()); - result = prime * result + (int)(measurementIdentifier ^ (measurementIdentifier >>> 32)); - return result; - } - - @Override - public String toString() { - return "MeasurementIdentifier [deviceIdentifier=" + deviceIdentifier + ", measurementIdentifier=" - + measurementIdentifier + "]"; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - var other = (MeasurementIdentifier)obj; - if (deviceIdentifier == null) { - if (other.deviceIdentifier != null) - return false; - } else if (!deviceIdentifier.equals(other.deviceIdentifier)) - return false; - return measurementIdentifier == other.measurementIdentifier; - } - - @Override - public int compareTo(final MeasurementIdentifier measurementIdentifier) { - final var deviceIdentifierComparison = this.getDeviceIdentifier() - .compareTo(measurementIdentifier.getDeviceIdentifier()); - return deviceIdentifierComparison == 0 - ? Long.compare(this.getMeasurementIdentifier(), measurementIdentifier.getMeasurementIdentifier()) - : deviceIdentifierComparison; - } -} diff --git a/libs/model/src/main/kotlin/de/cyface/model/MeasurementIdentifier.kt b/libs/model/src/main/kotlin/de/cyface/model/MeasurementIdentifier.kt new file mode 100644 index 0000000..f755caf --- /dev/null +++ b/libs/model/src/main/kotlin/de/cyface/model/MeasurementIdentifier.kt @@ -0,0 +1,134 @@ +/* + * Copyright 2021-2024 Cyface GmbH + * + * This file is part of the Serialization. + * + * The Serialization is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Serialization is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with the Serialization. If not, see . + */ +package de.cyface.model + +import org.apache.commons.lang3.Validate +import java.io.Serializable + +/** + * Describes the worldwide unique identifier of a measurement. This identifier consists of a device identifier and the + * actual measurement from that device. + * + * The natural order of [MeasurementIdentifier] is lexicographically by device identifier and then by + * measurement identifier. + * + * @author Klemens Muthmann + */ +class MeasurementIdentifier : Comparable, Serializable { + /** + * The worldwide unique identifier of the device that captured this measurement. + */ + private var deviceIdentifier: String? = null + + /** + * The device wide unique identifier of the measurement. + */ + private var measurementIdentifier: Long = 0 + + /** + * The default no arguments constructor as required by Apache Flink to serialize and deserialize objects of this + * class. Do not use this in you own code, it creates an unusable [MeasurementIdentifier]. + */ + constructor() + + /** + * Creates a new completely initialized object of this class. + * + * @param deviceIdentifier The worldwide unique identifier of the device that captured this measurement + * @param measurementIdentifier The device wide unique identifier of the measurement + */ + constructor(deviceIdentifier: String?, measurementIdentifier: Long) { + setDeviceIdentifier(deviceIdentifier) + setMeasurementIdentifier(measurementIdentifier) + } + + /** + * @return The worldwide unique identifier of the device that captured this measurement + */ + fun getDeviceIdentifier(): String? { + return deviceIdentifier + } + + /** + * @return The device wide unique identifier of the measurement + */ + fun getMeasurementIdentifier(): Long { + return measurementIdentifier + } + + /** + * @param deviceIdentifier The worldwide unique identifier of the device that captured this measurement + */ + @Suppress("MemberVisibilityCanBePrivate") // API + fun setDeviceIdentifier(deviceIdentifier: String?) { + Validate.notEmpty(deviceIdentifier) + + this.deviceIdentifier = deviceIdentifier + } + + /** + * @param measurementIdentifier The device wide unique identifier of the measurement + */ + @Suppress("MemberVisibilityCanBePrivate") // API + fun setMeasurementIdentifier(measurementIdentifier: Long) { + Validate.isTrue(measurementIdentifier >= 0) + + this.measurementIdentifier = measurementIdentifier + } + + override fun hashCode(): Int { + val prime = 31 + var result = 1 + result = prime * result + (if ((deviceIdentifier == null)) 0 else deviceIdentifier.hashCode()) + result = prime * result + (measurementIdentifier xor (measurementIdentifier ushr 32)).toInt() + return result + } + + override fun toString(): String { + return ("MeasurementIdentifier [deviceIdentifier=" + deviceIdentifier + ", measurementIdentifier=" + + measurementIdentifier + "]") + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null) return false + if (javaClass != other.javaClass) return false + val otherId = other as MeasurementIdentifier + if (deviceIdentifier == null) { + if (otherId.deviceIdentifier != null) return false + } else if (deviceIdentifier != otherId.deviceIdentifier) return false + return measurementIdentifier == otherId.measurementIdentifier + } + + override fun compareTo(other: MeasurementIdentifier): Int { + val deviceIdentifierComparison = getDeviceIdentifier()!! + .compareTo(other.getDeviceIdentifier()!!) + return if (deviceIdentifierComparison == 0) this.getMeasurementIdentifier() + .compareTo(other.getMeasurementIdentifier()) + else deviceIdentifierComparison + } + + companion object { + /** + * Used to serialize objects of this class. Only change this value if this classes attribute set changes. + */ + @Suppress("ConstPropertyName") + private const val serialVersionUID = 181303400330020850L + } +} diff --git a/libs/model/src/main/kotlin/de/cyface/model/RequestMetaData.kt b/libs/model/src/main/kotlin/de/cyface/model/RequestMetaData.kt index 3855567..c859062 100644 --- a/libs/model/src/main/kotlin/de/cyface/model/RequestMetaData.kt +++ b/libs/model/src/main/kotlin/de/cyface/model/RequestMetaData.kt @@ -19,151 +19,301 @@ package de.cyface.model +import de.cyface.model.RequestMetaData.MeasurementMetaData.GeoLocation import java.io.Serializable import java.nio.charset.Charset /** - * The metadata as transmitted in the request header or pre-request body. + * The metadata as transmitted in the request header or pre-request body for the measurement file. * * @author Armin Schnabel - * @property deviceType The worldwide unique identifier of the device uploading the data. - * @property measurementIdentifier The device wide unique identifier of the uploaded measurement. - * @property operatingSystemVersion The operating system version, such as Android 9.0.0 or iOS 11.2. - * @property deviceType The type of device uploading the data, such as Pixel 3 or iPhone 6 Plus. - * @property applicationVersion The version of the app that transmitted the measurement. - * @property length The length of the measurement in meters. - * @property locationCount The count of geolocations in the transmitted measurement. - * @property startLocation The `GeoLocation` at the beginning of the track represented by the transmitted measurement. - * @property endLocation The `GeoLocation` at the end of the track represented by the transmitted measurement. - * @property modality The modality type used to capture the measurement. - * @property formatVersion The format version of the upload file. - * @property logCount Count of log files captured for this measurement, e.g. metrics captured during image capturing. - * @property imageCount Count of images captured for this measurement. Allows to notice when all images are transmitted. - * @property videoCount Count of videos captured for this measurement. Allows to notice when all videos are transmitted. - * @property filesSize The number of bytes of the files collected for this measurement (log, image and video data). - * @property attachmentIdentifier The identifier of the attachment, if this measurement is an attachment. + * @property identifier The identifier which identifies the data object transmitted. */ @Suppress("unused") // Part of the API -data class RequestMetaData( - val deviceIdentifier: String, - val measurementIdentifier: String, - val operatingSystemVersion: String, - val deviceType: String, - val applicationVersion: String, - val length: Double, - val locationCount: Long, - val startLocation: GeoLocation?, - val endLocation: GeoLocation?, - val modality: String, - val formatVersion: Int, - val logCount: Int, - val imageCount: Int, - val videoCount: Int, - val filesSize: Long, - val attachmentIdentifier: String? = null, +data class RequestMetaData( + val identifier: T, + val deviceMetaData: DeviceMetaData, + val applicationMetaData: ApplicationMetaData, + val measurementMetaData: MeasurementMetaData, + val attachmentMetaData: AttachmentMetaData, ) : Serializable { + /** + * An identifier which is passed in [RequestMetaData] to identify which data object is transmitted. + * + * @author Armin Schnabel + */ + interface Identifier : Serializable { + companion object { + /** + * The length of a universal unique identifier. + */ + const val UUID_LENGTH = 36 - init { - require(deviceIdentifier.toByteArray(Charset.forName(DEFAULT_CHARSET)).size == UUID_LENGTH) { - "Field deviceId was not exactly 128 Bit, which is required for UUIDs!" - } - require(deviceType.isNotEmpty() && deviceType.length <= MAX_GENERIC_METADATA_FIELD_LENGTH) { - "Field deviceType had an invalid length of ${deviceType.length.toLong()}" + /** + * The default char set to use for encoding and decoding strings transmitted as metadata. + */ + const val DEFAULT_CHARSET = "UTF-8" + + /** + * The maximum length of the measurement or attachment identifier in characters (this is the amount of + * characters of {@value Long#MAX_VALUE}). + */ + const val MAX_ID_LENGTH = 20 } - require(measurementIdentifier.isNotEmpty() && measurementIdentifier.length <= MAX_ID_LENGTH) { - "Field measurementId had an invalid length of ${measurementIdentifier.length.toLong()}" + } + + /** + * An identifier for a transmitted measurement file. + * + * @author Armin Schnabel + * @property deviceId The world-unique identifier of the device which collected the data. + * @property measurementId The device-unique identifier of the measurement transmitted. + */ + data class MeasurementIdentifier( + val deviceId: String, + val measurementId: String, + ) : Identifier { + + init { + require( + deviceId.toByteArray(Charset.forName(Identifier.DEFAULT_CHARSET)).size == Identifier.UUID_LENGTH + ) { + "Field deviceId was not exactly 128 Bit, which is required for UUIDs!" + } + require(measurementId.isNotEmpty() && measurementId.length <= Identifier.MAX_ID_LENGTH) { + "Field measurementId had an invalid length of ${measurementId.length.toLong()}" + } } - require(operatingSystemVersion.isNotEmpty() && - operatingSystemVersion.length <= MAX_GENERIC_METADATA_FIELD_LENGTH) { - "Field osVersion had an invalid length of ${operatingSystemVersion.length.toLong()}" + + companion object { + /** + * Used to serialize objects of this class. Only change this value if this classes attribute set changes. + */ + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L } - require(applicationVersion.isNotEmpty() && applicationVersion.length <= MAX_GENERIC_METADATA_FIELD_LENGTH) { - "Field applicationVersion had an invalid length of ${applicationVersion.length.toLong()}" + } + + /** + * An identifier for a transmitted attachment file. + * + * @author Armin Schnabel + * @property attachmentId The device-unique identifier of the attachment file transmitted. + */ + data class AttachmentIdentifier( + val attachmentId: String, + ) : Identifier { + init { + require(attachmentId.isNotEmpty() && attachmentId.length <= Identifier.MAX_ID_LENGTH) { + "Field attachmentId had an invalid length of ${attachmentId.length.toLong()}" + } } - require(length >= MINIMUM_TRACK_LENGTH) { - "Field length had an invalid value smaller then 0.0: $length" + + companion object { + /** + * Used to serialize objects of this class. Only change this value if this classes attribute set changes. + */ + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L } - require(locationCount >= MINIMUM_LOCATION_COUNT) { - "Field locationCount had an invalid value smaller then 0: $locationCount" + } + + /** + * The metadata which describes the device which collected the data. + * + * @author Armin Schnabel + * @property operatingSystemVersion The operating system version, such as Android 9.0.0 or iOS 11.2. + * @property deviceType The type of device uploading the data, such as Pixel 3 or iPhone 6 Plus. + */ + data class DeviceMetaData( + val operatingSystemVersion: String, + val deviceType: String, + ) : Serializable { + init { + require( + operatingSystemVersion.isNotEmpty() && + operatingSystemVersion.length <= MAX_GENERIC_METADATA_FIELD_LENGTH + ) { + "Field osVersion had an invalid length of ${operatingSystemVersion.length.toLong()}" + } + require(deviceType.isNotEmpty() && deviceType.length <= MAX_GENERIC_METADATA_FIELD_LENGTH) { + "Field deviceType had an invalid length of ${deviceType.length.toLong()}" + } } - require(locationCount == MINIMUM_LOCATION_COUNT || startLocation != null) { - "Start location should only be defined if there is at least one location in the uploaded track!" + + companion object { + /** + * Used to serialize objects of this class. Only change this value if this classes attribute set changes. + */ + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L } - require(locationCount == MINIMUM_LOCATION_COUNT || endLocation != null) { - "End location should only be defined if there is at least one location in the uploaded track!" + } + + /** + * The metadata which describes the application which collected the data. + * + * @author Armin Schnabel + * @property applicationVersion The version of the app that transmitted the measurement. + * @property formatVersion The format version of the upload file. + */ + data class ApplicationMetaData( + val applicationVersion: String, + val formatVersion: Int, + ) : Serializable { + init { + require(applicationVersion.isNotEmpty() && applicationVersion.length <= MAX_GENERIC_METADATA_FIELD_LENGTH) { + "Field applicationVersion had an invalid length of ${applicationVersion.length.toLong()}" + } + require(formatVersion == CURRENT_TRANSFER_FILE_FORMAT_VERSION) { + "Unsupported formatVersion: ${formatVersion.toLong()}" + } } - require(modality.isNotEmpty() && modality.length <= MAX_GENERIC_METADATA_FIELD_LENGTH) { - "Field modality had an invalid length of ${modality.length.toLong()}" + + companion object { + /** + * Used to serialize objects of this class. Only change this value if this classes attribute set changes. + */ + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + + /** + * The current version of the transferred file. This is always specified by the first two bytes of the file + * transferred and helps compatible APIs to process data from different client versions. + */ + const val CURRENT_TRANSFER_FILE_FORMAT_VERSION = 3 } - require(formatVersion == CURRENT_TRANSFER_FILE_FORMAT_VERSION) { - "Unsupported formatVersion: ${formatVersion.toLong()}" + } + + /** + * The metadata which describes the measurement the data was collected for. + * + * @author Armin Schnabel + * @property length The length of the measurement in meters. + * @property locationCount The count of geolocations in the transmitted measurement. + * @property startLocation The first [GeoLocation] captured by the transmitted measurement. + * @property endLocation The last [GeoLocation] captured by the transmitted measurement. + * @property modality The modality type used to capture the measurement. + */ + data class MeasurementMetaData( + val length: Double, + val locationCount: Long, + val startLocation: GeoLocation?, + val endLocation: GeoLocation?, + val modality: String, + ) : Serializable { + init { + require(length >= MINIMUM_TRACK_LENGTH) { + "Field length had an invalid value smaller then 0.0: $length" + } + require(locationCount >= MINIMUM_LOCATION_COUNT) { + "Field locationCount had an invalid value smaller then 0: $locationCount" + } + require(locationCount == MINIMUM_LOCATION_COUNT || startLocation != null) { + "Start location should only be defined if there is at least one location in the uploaded track!" + } + require(locationCount == MINIMUM_LOCATION_COUNT || endLocation != null) { + "End location should only be defined if there is at least one location in the uploaded track!" + } + require(modality.isNotEmpty() && modality.length <= MAX_GENERIC_METADATA_FIELD_LENGTH) { + "Field modality had an invalid length of ${modality.length.toLong()}" + } } - require(logCount >= 0) { "Invalid logCount: $logCount" } - require(imageCount >= 0) { "Invalid imageCount: $imageCount" } - require(videoCount >= 0) { "Invalid videoCount: $videoCount" } - require(filesSize >= 0) { "Invalid filesSize: $filesSize" } - require(attachmentIdentifier == null || attachmentIdentifier.length <= MAX_ID_LENGTH) { - "Field attachmentId had an invalid length of ${attachmentIdentifier!!.length.toLong()}" + + /** + * This class represents a geolocation at the start or end of a track. + * + * @author Armin Schnabel + * @property timestamp The Unix timestamp this location was captured on in milliseconds. + * @property latitude Geographical latitude (decimal fraction) raging from -90° (south) to 90° (north). + * @property longitude Geographical longitude (decimal fraction) ranging from -180° (west) to 180° (east). + */ + data class GeoLocation( + val timestamp: Long, + val latitude: Double, + val longitude: Double + ) + + companion object { + /** + * Used to serialize objects of this class. Only change this value if this classes attribute set changes. + */ + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + + /** + * The minimum length of a track stored with a measurement. + */ + private const val MINIMUM_TRACK_LENGTH = 0.0 + + /** + * The minimum valid amount of locations stored inside a measurement. + */ + private const val MINIMUM_LOCATION_COUNT = 0L } } /** - * This class represents a geolocation at the start or end of a track. + * The metadata which describes the attachments which were collected together with the measurement. * * @author Armin Schnabel - * @property timestamp The timestamp this location was captured on in milliseconds since 1st January 1970 (epoch). - * @property latitude Geographical latitude (decimal fraction) raging from -90° (south) to 90° (north). - * @property longitude Geographical longitude (decimal fraction) ranging from -180° (west) to 180° (east). + * @property logCount Number of log files captured for this measurement, e.g. image capturing metrics. + * @property imageCount Number of image files captured for this measurement. + * @property videoCount Number of video files captured for this measurement. + * @property filesSize The number of bytes of the files collected for this measurement (log, image and video data). */ - data class GeoLocation( - val timestamp: Long, - val latitude: Double, - val longitude: Double - ) + data class AttachmentMetaData( + val logCount: Int, + val imageCount: Int, + val videoCount: Int, + val filesSize: Long, + ) : Serializable { + init { + require(logCount >= 0) { "Invalid logCount: $logCount" } + require(imageCount >= 0) { "Invalid imageCount: $imageCount" } + require(videoCount >= 0) { "Invalid videoCount: $videoCount" } + require(filesSize >= 0) { "Invalid filesSize: $filesSize" } + } + + companion object { + /** + * Used to serialize objects of this class. Only change this value if this classes attribute set changes. + */ + @Suppress("ConstPropertyName") + private const val serialVersionUID = 1L + } + } companion object { /** * Used to serialize objects of this class. Only change this value if this classes attribute set changes. */ @Suppress("ConstPropertyName") - private const val serialVersionUID = -1700430112854515404L - - /** - * The length of a universal unique identifier. - */ - private const val UUID_LENGTH = 36 - - /** - * The default char set to use for encoding and decoding strings transmitted as metadata. - */ - private const val DEFAULT_CHARSET = "UTF-8" + private const val serialVersionUID = 2L /** * Maximum size of a metadata field, with plenty space for future development. Prevents attackers from putting * arbitrary long data into these fields. */ const val MAX_GENERIC_METADATA_FIELD_LENGTH = 30 + } - /** - * The maximum length of the measurement or attachment identifier in characters (this is the amount of - * characters of {@value Long#MAX_VALUE}). - */ - private const val MAX_ID_LENGTH = 20 + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false - /** - * The minimum length of a track stored with a measurement. - */ - private const val MINIMUM_TRACK_LENGTH = 0.0 + other as RequestMetaData<*> - /** - * The minimum valid amount of locations stored inside a measurement. - */ - private const val MINIMUM_LOCATION_COUNT = 0L + if (identifier != other.identifier) return false + if (deviceMetaData != other.deviceMetaData) return false + if (applicationMetaData != other.applicationMetaData) return false + if (measurementMetaData != other.measurementMetaData) return false + if (attachmentMetaData != other.attachmentMetaData) return false - /** - * The current version of the transferred file. This is always specified by the first two bytes of the file - * transferred and helps compatible APIs to process data from different client versions. - */ - const val CURRENT_TRANSFER_FILE_FORMAT_VERSION = 3 + return true + } + + override fun hashCode(): Int { + return identifier.hashCode() } }