Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,15 @@ class GatewayHook : AbstractGatewayModuleHook() {
}

PyDatasetBuilder.register()

context.webResourceManager.addServlet(HistoryServlet.PATH, HistoryServlet::class.java)
}

override fun startup(activationState: LicenseState) {}
override fun startup(activationState: LicenseState) = Unit

override fun shutdown() {
PyDatasetBuilder.unregister()
context.webResourceManager.removeServlet(HistoryServlet.PATH)
}

override fun initializeScriptManager(manager: ScriptManager) {
Expand All @@ -52,6 +56,6 @@ class GatewayHook : AbstractGatewayModuleHook() {
}
}

override fun isFreeModule(): Boolean = true
override fun isMakerEditionCompatible(): Boolean = true
override fun isFreeModule() = true
override fun isMakerEditionCompatible() = true
}
162 changes: 162 additions & 0 deletions gateway/src/main/kotlin/org/imdc/extensions/gateway/HistoryServlet.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package org.imdc.extensions.gateway

import com.inductiveautomation.ignition.common.QualifiedPathUtils
import com.inductiveautomation.ignition.common.StreamingDatasetWriter
import com.inductiveautomation.ignition.common.gson.stream.JsonWriter
import com.inductiveautomation.ignition.common.model.values.QualityCode
import com.inductiveautomation.ignition.common.sqltags.history.AggregationMode
import com.inductiveautomation.ignition.common.sqltags.history.BasicTagHistoryQueryParams
import com.inductiveautomation.ignition.common.sqltags.history.ReturnFormat
import com.inductiveautomation.ignition.common.util.LoggerEx
import com.inductiveautomation.ignition.gateway.model.GatewayContext
import org.apache.http.entity.ContentType
import java.io.PrintWriter
import java.text.SimpleDateFormat
import java.time.Instant
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
import java.util.Date
import javax.servlet.http.HttpServlet
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

class HistoryServlet : HttpServlet() {
private lateinit var context: GatewayContext

override fun init() {
context = servletContext.getAttribute(GatewayContext.SERVLET_CONTEXT_KEY) as GatewayContext
}

override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
resp.contentType = ContentType.APPLICATION_JSON.toString()
resp.writer.use { writer ->
val historyQuery: BasicTagHistoryQueryParams =
try {
val paths =
req.getParameterValues("path")
?: throw IllegalArgumentException("Must specify at least one path")
val startDate =
req.getParameter("startDate")?.toDate() ?: Date.from(Instant.now().minus(8, ChronoUnit.HOURS))
val endDate = req.getParameter("endDate")?.toDate() ?: Date()
val returnSize = req.getParameter("returnSize")?.toInt() ?: -1
val aggregationMode =
req.getParameter("aggregationMode")?.let(AggregationMode::valueOf) ?: AggregationMode.Average
val aliases = req.getParameter("aliases")?.split(',')

BasicTagHistoryQueryParams(
paths.map(QualifiedPathUtils::toPathFromHistoricalString),
startDate,
endDate,
returnSize,
aggregationMode,
ReturnFormat.Wide,
aliases,
emptyList(),
)
} catch (e: Exception) {
resp.status = HttpServletResponse.SC_BAD_REQUEST
e.printStackTrace(PrintWriter(writer))
return
}

try {
context.tagHistoryManager.queryHistory(
historyQuery,
StreamingJsonWriter(
JsonWriter(writer),
),
)
} catch (e: TrialExpiredException) {
resp.status = HttpServletResponse.SC_PAYMENT_REQUIRED
logger.error("Tag historian module reported trial expired", e)
} catch (e: Exception) {
resp.status = HttpServletResponse.SC_INTERNAL_SERVER_ERROR
logger.error("Unexpected exception writing JSON content to servlet", e)
}
}
}

class TrialExpiredException : Exception()

class StreamingJsonWriter(private val jsonWriter: JsonWriter) : StreamingDatasetWriter {
private lateinit var names: Array<String>
private lateinit var types: Array<Class<*>>

private val test = DateTimeFormatter.ISO_INSTANT

override fun initialize(
columnNames: Array<String>,
columnTypes: Array<Class<*>>,
hasQuality: Boolean,
expectedRows: Int,
) {
this.names = columnNames
this.types = columnTypes

jsonWriter.beginArray()
}

override fun write(data: Array<out Any?>, quality: Array<out QualityCode>): Unit = jsonWriter.run {
if (quality.any { it.`is`(QualityCode.Bad_TrialExpired) }) {
throw TrialExpiredException()
}

writeObject {
for (index in data.indices) {
name(names[index])
when (val value = data[index]) {
is Number -> {
when (types[index]) {
Float::class.java, Double::class.java -> value(value.toDouble())
else -> value(value.toLong())
}
}

is Date -> {
value(test.format(value.toInstant()))
}

is String -> value(value)
is Boolean -> value(value)

null -> nullValue()
}
}
}
}

override fun finish() {
jsonWriter.endArray()
}

override fun finishWithError(exception: java.lang.Exception) = throw exception

private inline fun JsonWriter.writeObject(block: JsonWriter.() -> Unit) {
beginObject()
block()
endObject()
}
}

companion object {
const val PATH = "history-extension"

private val logger = LoggerEx.newBuilder().build(HistoryServlet::class.java)

private val parsingStrategies = listOf<(String) -> Date>(
SimpleDateFormat.getDateTimeInstance()::parse,
SimpleDateFormat.getInstance()::parse,
{ Date(it.toLong()) },
)

private fun String.toDate(): Date? {
return parsingStrategies.firstNotNullOfOrNull { strategy ->
try {
strategy.invoke(this)
} catch (e: Exception) {
null
}
}
}
}
}