KafkaTopology.kt 19.6 KB
Newer Older
Jonas Waeber's avatar
Jonas Waeber committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
 * Media Linker
 * Copyright (C) 2020 Memoriav
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package org.memobase

Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
21
22
23
24
import java.io.StringReader
import java.io.StringWriter
import java.net.MalformedURLException
import java.net.URL
Jonas Waeber's avatar
Jonas Waeber committed
25
26
27
import org.apache.jena.rdf.model.Model
import org.apache.jena.rdf.model.ModelFactory
import org.apache.jena.rdf.model.Resource
28
import org.apache.jena.rdf.model.ResourceFactory
29
import org.apache.jena.riot.RiotException
Jonas Waeber's avatar
Jonas Waeber committed
30
31
import org.apache.kafka.streams.StreamsBuilder
import org.apache.kafka.streams.kstream.Predicate
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
32
import org.apache.logging.log4j.LogManager
Jonas Waeber's avatar
Jonas Waeber committed
33
import org.memobase.rdf.EBUCORE
34
import org.memobase.rdf.RDF
Jonas Waeber's avatar
Jonas Waeber committed
35
36
37
38
39
import org.memobase.rdf.RICO
import org.memobase.reports.ReportMessages
import org.memobase.reports.ReportStatus
import org.memobase.settings.SettingsLoader
import org.memobase.sftp.SftpClient
40
41
import settings.HeaderExtractionTransformSupplier
import settings.HeaderMetadata
Jonas Waeber's avatar
Jonas Waeber committed
42
43

class KafkaTopology(private val settings: SettingsLoader) {
44
    private val appSettings = settings.appSettings
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
45
    private val log = LogManager.getLogger("KafkaTopology")
46

Jonas Waeber's avatar
Jonas Waeber committed
47
    private val sftpClient = SftpClient(settings.sftpSettings)
48
    private val previewImageHandler = RemoteResourceHandler(sftpClient)
49
    private val sftpBasePath = appSettings.getProperty(Constant.sftpBasePathPropertyName)
50
    private val fileExtensions = appSettings.getProperty(Constant.extensionsPropertyName).split(",")
51
    private val reportingTopic = settings.processReportTopic
Jonas Waeber's avatar
Jonas Waeber committed
52

53
    fun prepare(): StreamsBuilder {
Jonas Waeber's avatar
Jonas Waeber committed
54
55
        val builder = StreamsBuilder()

Jonas Waeber's avatar
Jonas Waeber committed
56
        val stream = builder.stream<String, String>(settings.inputTopic)
Jonas Waeber's avatar
Jonas Waeber committed
57

58
        val model = stream
59
            .transformValues(HeaderExtractionTransformSupplier<String>())
60
            .mapValues { value -> createModel(value) }
61
62
63
64
65
66
67
            .branch(
                Predicate { _, value -> value != null },
                Predicate { _, _ -> true }
            )

        model[1]
            .mapValues { key, _ ->
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
68
                log.warn("Invalid input data. Check mapper service processing.")
69
70
                Report(
                    key,
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
71
                    ReportStatus.fatal,
72
                    generalFailureMessage = "Invalid input data. Check mapper service processing."
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
73
                ).toJson()
74
75
76
            }
            .to(reportingTopic)

77
        val requiredFieldsAvailable = model[0]
78
            .mapValues { value -> extractSubjects(value!!) }
79
80
81
82
83
            .mapValues { key, value ->
                if (getDigitalObjectResource(value.second) == null) {
                    createRecord(
                        value,
                        key,
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
84
85
                        ReportStatus.warning,
                        generalFailureMessage = "No digital object resource present in model."
86
87
88
89
90
91
92
                    )
                } else {
                    createRecord(value, key, ReportStatus.success)
                }
            }
            .mapValues { key, value ->
                val recordResource = getRecordResource(value.second)
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
                when {
                    recordResource == null -> {
                        updateRecord(
                            value,
                            ReportStatus.fatal,
                            generalMessage = "No record resource present in model."
                        )
                    }
                    getOriginalIdentifier(recordResource) == null -> {
                        updateRecord(
                            value,
                            ReportStatus.fatal,
                            generalMessage = ReportMessages.noOriginalIdentifier(key)
                        )
                    }
                    else -> {
                        value
                    }
111
                }
Matthias's avatar
Matthias committed
112
            }
113
            .branch(
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
114
115
                Predicate { _, value -> value.third.status == ReportStatus.fatal },
                Predicate { _, value -> value.third.status == ReportStatus.warning },
116
                Predicate { _, _ -> true }
117
            )
Jonas Waeber's avatar
Jonas Waeber committed
118

119
        requiredFieldsAvailable[0]
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
120
121
122
123
124
125
126
            .mapValues { _, value ->
                log.warn("Record contains faulty data: ${value.third.digitalObjectMessage}. Abort processing of message")
                value.third.toJson()
            }
            .to(reportingTopic)

        requiredFieldsAvailable[1]
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
127
            .mapValues { _, value ->
128
                log.warn("Record contains faulty data: ${value.third.digitalObjectMessage}")
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
129
130
131
                value.third.toJson()
            }
            .to(reportingTopic)
132

Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
133
        requiredFieldsAvailable[1]
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
134
135
136
            .mapValues { value -> serializeModel(value.first.first) }
            .to(settings.outputTopic)

Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
137
        val hasLocatorBranch = requiredFieldsAvailable[2]
138
139
140
141
142
143
144
145
146
147
148
149
            .mapValues { readOnlyKey, value ->
                addThumbnailSftpLocatorToModel(
                    readOnlyKey,
                    value
                )
            }
            .branch(
                Predicate { _, value -> hasDigitalObjectWithoutLocator(value.second) }, // Indicates a local media file
                Predicate { _, _ -> true } // Indicates a remote media file; check for youtube / vimeo thumbnail fetching
            )

        val updateDigitalObjects = hasLocatorBranch[0]
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
150
151
            .mapValues { readOnlyKey, value ->
                val enrichedModel = addMediaSftpLocatorToModel(readOnlyKey, value)
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
152
                if (enrichedModel.third.status == ReportStatus.warning) {
153
                    log.warn("A problem enriching the digital object occurred: ${enrichedModel.third.digitalObjectMessage}")
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
154
155
                }
                enrichedModel
156
            }
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
157
158
159

        updateDigitalObjects
            .mapValues { value -> serializeModel(value.first.first) }
160
            .to(settings.outputTopic)
Jonas Waeber's avatar
Jonas Waeber committed
161
162

        updateDigitalObjects
163
            .mapValues { value -> value.third.toJson() }
164
            .to(reportingTopic)
Jonas Waeber's avatar
Jonas Waeber committed
165

166
        hasLocatorBranch[1]
167
            .mapValues { value -> fetchThumbnailForYoutubeOrVimeoFile(value) }
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
168
            .mapValues { value -> serializeModel(value.first.first) }
169
            .to(settings.outputTopic)
Jonas Waeber's avatar
Jonas Waeber committed
170

171
        hasLocatorBranch[1]
172
            .mapValues { _, value -> value.third.toJson() }
173
            .to(reportingTopic)
Jonas Waeber's avatar
Jonas Waeber committed
174

175
        return builder
Jonas Waeber's avatar
Jonas Waeber committed
176
177
    }

178
179
    private fun fetchThumbnailForYoutubeOrVimeoFile(value: Triple<Pair<Model, HeaderMetadata>, List<Resource>, Report>): Triple<Pair<Model, HeaderMetadata>, List<Resource>, Report> {
        if (noThumbnailAttached(value.second)) {
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
180
181
182
183
            val recordResource = getRecordResource(value.second)
            val digitalObjectResource = getDigitalObjectResource(value.second)
            if (recordResource != null && digitalObjectResource != null) {
                val locator = digitalObjectResource.getProperty(EBUCORE.locator).string
184
                when {
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
185
186
                    isNoValidUrl(locator) -> {
                        log.warn("No valid locator url found for ${value.third.id}")
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
187
                        return updateRecord(value, ReportStatus.warning, thumbnailMessage = "no valid locator url")
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
188
                    }
189
                    RemoteResourceHandler.isVimeoUrl(locator) -> {
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
190
                        log.info("Trying to download thumbnail file on vimeo for ${value.third.id}")
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
191
192
193
194
                        val thumbnailHandler = this.previewImageHandler.getFromVimeo(locator)
                        if (thumbnailHandler == null) {
                            log.warn("Download for ${value.third.id} failed!")
                            return updateRecord(
195
                                value,
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
196
                                ReportStatus.warning,
197
                                thumbnailMessage = "couldn't fetch vimeo thumbnail"
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
198
                            )
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
199
200
                        }
                        thumbnailHandler
201
                    }
202
                    RemoteResourceHandler.isYoutubeUrl(locator) -> {
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
203
                        log.info("Trying to download thumbnail file on youtube for ${value.third.id}")
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
204
205
206
207
                        val thumbnailHandler = this.previewImageHandler.getFromYoutube(locator)
                        if (thumbnailHandler == null) {
                            log.warn("Download for ${value.third.id} failed!")
                            return updateRecord(
208
                                value,
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
209
210
                                ReportStatus.warning,
                                thumbnailMessage = "couldn't fetch youtube thumbnail"
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
211
                            )
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
212
213
                        }
                        thumbnailHandler
214
215
                    }
                    else -> {
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
216
                        log.debug("Won't fetch thumbnail file for ${value.third.id} because no youtube/vimeo resource")
217
218
                        return updateRecord(
                            value,
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
219
                            ReportStatus.success,
220
                            thumbnailMessage = "no additional thumbnails fetched"
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
221
                        )
222
                    }
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
223
224
225
226
227
228
229
                }.let { h ->
                    val enrichedValue = addDimensionsToDigitalObject(value, h.first)
                    val filePath = h.second
                    return if (filePath != null) {
                        addLocalThumbnail(enrichedValue, recordResource, digitalObjectResource, filePath)
                    } else {
                        log.warn("No thumbnail url available for ${value.third.id}")
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
230
231
232
233
234
                        updateRecord(
                            enrichedValue,
                            ReportStatus.warning,
                            thumbnailMessage = "Download of youtube / vimeo thumbnail failed. Check if resource is still available."
                        )
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
235
                    }
236
237
238
                }
            }
        }
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
239
        return value
240
241
    }

242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
    private fun addLocalThumbnail(
        value: Triple<Pair<Model, HeaderMetadata>, List<Resource>, Report>,
        recordResource: Resource,
        digitalObjectResource: Resource,
        pathToLocalFile: String
    ): Triple<Pair<Model, HeaderMetadata>, List<Resource>, Report> {
        val destPath = "$sftpBasePath/${value.first.second.recordSetId}/${Constant.thumbnailFolderName}/${
            recordResource.uri.split(
                "/"
            ).last()
        }.jpg"
        val pathOnSftpServer = previewImageHandler.moveFileToSFTP(pathToLocalFile, destPath)
        if (pathOnSftpServer != null) {
            log.info("Move downloaded thumbnail file to $destPath for ${value.third.id}")
            createThumbnailResource(
                value.first.first,
                recordResource,
                digitalObjectResource,
                pathOnSftpServer
            )
            return updateRecord(
                value,
                value.third.status,
                thumbnailMessage = "youtube / vimeo thumbnail fetched"
            )
        } else {
            log.warn("Couldn't move downloaded thumbnail file to $destPath for ${value.third.id}")
            return updateRecord(
                value,
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
271
                ReportStatus.warning,
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
                thumbnailMessage = "upload of youtube / vimeo thumbnail to sFTP server failed"
            )
        }
    }

    private fun addDimensionsToDigitalObject(
        value: Triple<Pair<Model, HeaderMetadata>, List<Resource>, Report>,
        oembedObject: OembedResponse
    ): Triple<Pair<Model, HeaderMetadata>, List<Resource>, Report> {
        val digitalObjectResource = getDigitalObjectResource(value.second)!!
        // TODO
        if (oembedObject.width != null) {
            val width = ResourceFactory.createPlainLiteral(oembedObject.width.toString())
            digitalObjectResource.addLiteral(EBUCORE.width, width)
        }
        if (oembedObject.height != null) {
            val height = ResourceFactory.createPlainLiteral(oembedObject.height.toString())
            digitalObjectResource.addLiteral(EBUCORE.height, height)
        }
        value.first.first.createLiteral(digitalObjectResource.toString(), true)
        return value
    }

295
296
297
298
    private fun noThumbnailAttached(resources: List<Resource>): Boolean {
        return resources.none { it.hasProperty(RICO.type, Constant.thumbnailRicoType) }
    }

299
300
    private fun extractSubjects(input: Pair<Model, HeaderMetadata>): Pair<Pair<Model, HeaderMetadata>, List<Resource>> {
        return Pair(input, input.first.listSubjects().toList())
Jonas Waeber's avatar
Jonas Waeber committed
301
302
    }

Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
303
    private fun hasDigitalObjectWithoutLocator(res: List<Resource>): Boolean {
304
305
306
        return res.any { it.hasProperty(RICO.type, Constant.digitalObject) && !it.hasProperty(EBUCORE.locator) }
    }

Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
307
    private fun getOriginalIdentifier(record: Resource): String? {
308
        return record.listProperties(RICO.identifiedBy).toList().map { statement -> statement.`object`.asResource() }
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
309
            .firstOrNull { resource ->
310
311
312
313
                resource.hasProperty(RDF.type, RICO.Identifier) && resource.hasProperty(
                    RICO.type,
                    Constant.identifierType
                )
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
314
            }?.getProperty(RICO.identifier)?.string
Jonas Waeber's avatar
Jonas Waeber committed
315
316
    }

317
    private fun createModel(data: Pair<String, HeaderMetadata>): Pair<Model, HeaderMetadata>? {
Jonas Waeber's avatar
Jonas Waeber committed
318
        val model = ModelFactory.createDefaultModel()
319
320
321
322
323
        try {
            model.read(StringReader(data.first), "", Constant.rdfParserLang)
        } catch (ex: RiotException) {
            return null
        }
324
        return Pair(model, data.second)
Jonas Waeber's avatar
Jonas Waeber committed
325
326
    }

327
328
329
330
331
    private fun createThumbnailResource(
        data: Model,
        record: Resource,
        digitalObject: Resource,
        locator: String
332
333
334
    ): String {
        val uri = "${digitalObject.uri}/derived"
        val thumbnail = data.createResource(uri)
335
336
337
338
339
340
341
342
        val literal = ResourceFactory.createPlainLiteral(locator)
        thumbnail.addProperty(RDF.type, RICO.Instantiation)
        thumbnail.addProperty(RICO.type, Constant.thumbnailRicoType)
        thumbnail.addProperty(EBUCORE.locator, literal)
        digitalObject.addProperty(RICO.hasDerivedInstantiation, thumbnail)
        thumbnail.addProperty(RICO.isDerivedFromInstantiation, digitalObject)
        record.addProperty(RICO.hasInstantiation, thumbnail)
        thumbnail.addProperty(RICO.instantiates, record)
343
        return uri
344
345
    }

Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
    private fun addThumbnailSftpLocatorToModel(
        key: String,
        data: Triple<Pair<Model, HeaderMetadata>, List<Resource>, Report>
    ): Triple<Pair<Model, HeaderMetadata>, List<Resource>, Report> {
        return addSftpLocatorToModel(key, data, Constant.thumbnailFolderName)
    }

    private fun addMediaSftpLocatorToModel(
        key: String,
        data: Triple<Pair<Model, HeaderMetadata>, List<Resource>, Report>
    ): Triple<Pair<Model, HeaderMetadata>, List<Resource>, Report> {
        return addSftpLocatorToModel(key, data, Constant.mediaFolderName)
    }

    private fun addSftpLocatorToModel(
Matthias's avatar
Matthias committed
361
362
        key: String,
        data: Triple<Pair<Model, HeaderMetadata>, List<Resource>, Report>,
363
        type: String
Matthias's avatar
Matthias committed
364
    ): Triple<Pair<Model, HeaderMetadata>, List<Resource>, Report> {
365
366
367
        val recordResource = getRecordResource(data.second)!!
        val digitalObjectResource = getDigitalObjectResource(data.second)!!
        val originalIdentifierValue = getOriginalIdentifier(recordResource)!!
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
368
        val link = getLinkToResourceOnSFTPServer(data.first.second.recordSetId, type, originalIdentifierValue)
369
            ?: return if (type == Constant.thumbnailFolderName) {
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
370
                updateRecord(data, ReportStatus.ignore, thumbnailMessage = "no local thumbnails available")
371
            } else {
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
372
                updateRecord(data, ReportStatus.warning, digitalObjectMessage = ReportMessages.reportFailure(key, type))
373
374
            }
        return if (type == Constant.mediaFolderName) {
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
375
            addLocatorToDigitalObjectResource(data.first.first, link, digitalObjectResource)
376
377
            updateRecord(
                data,
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
378
                ReportStatus.success,
379
                digitalObjectMessage = ReportMessages.reportSuccess(digitalObjectResource.uri, link, type)
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
380
            )
381
382
383
384
385
        } else {
            val uri = createThumbnailResource(data.first.first, recordResource, digitalObjectResource, link)
            updateRecord(
                data,
                ReportStatus.success,
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
386
                thumbnailMessage = ReportMessages.reportSuccess(uri, link, type)
387
388
            )
        }
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
389
390
391
392
393
394
395
396
397
398
399
    }

    private fun getLinkToResourceOnSFTPServer(
        recordSetId: String,
        type: String,
        originalIdentifierValue: String
    ): String? {
        for (extension in fileExtensions) {
            val filePath = "$sftpBasePath/$recordSetId/$type/$originalIdentifierValue.$extension"
            if (sftpClient.exists(filePath)) {
                return "${Constant.sftpPathPrefix}$filePath"
Jonas Waeber's avatar
Jonas Waeber committed
400
            }
Jonas Waeber's avatar
Jonas Waeber committed
401
        }
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
        return null
    }

    private fun getRecordResource(resources: List<Resource>): Resource? {
        return resources.firstOrNull { it.hasProperty(RDF.type, RICO.Record) }
    }

    private fun getDigitalObjectResource(resources: List<Resource>): Resource? {
        return resources.firstOrNull { it.hasProperty(RICO.type, Constant.digitalObject) }
    }

    private fun isNoValidUrl(locator: String): Boolean {
        return try {
            URL(locator)
            false
        } catch (ex: MalformedURLException) {
            true
        }
    }

    private fun addLocatorToDigitalObjectResource(model: Model, sftpLink: String, digitalObjectResource: Resource) {
        val literal = ResourceFactory.createPlainLiteral(sftpLink)
        digitalObjectResource.addLiteral(EBUCORE.locator, literal)
        model.createLiteral(digitalObjectResource.toString(), true)
    }

    private fun serializeModel(model: Model): String {
        val out = StringWriter()
        model.write(out, Constant.rdfParserLang)
        return out.toString().trim()
Jonas Waeber's avatar
Jonas Waeber committed
432
    }
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453

    private fun updateRecord(
        value: Triple<Pair<Model, HeaderMetadata>, List<Resource>, Report>,
        status: String,
        generalMessage: String = "",
        digitalObjectMessage: String = "",
        thumbnailMessage: String = ""
    ): Triple<Pair<Model, HeaderMetadata>, List<Resource>, Report> {
        val amendedReport = value.third.copy(
            status = status,
            generalFailureMessage = if (generalMessage != "") generalMessage else value.third.generalFailureMessage,
            digitalObjectMessage = if (digitalObjectMessage != "") digitalObjectMessage else value.third.digitalObjectMessage,
            thumbnailMessage = if (thumbnailMessage != "") thumbnailMessage else value.third.thumbnailMessage
        )
        return value.copy(third = amendedReport)
    }

    private fun createRecord(
        value: Pair<Pair<Model, HeaderMetadata>, List<Resource>>,
        messageId: String,
        status: String,
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
454
        generalFailureMessage: String = "",
455
456
457
458
459
460
        digitalObjectMessage: String = "",
        thumbnailMessage: String = ""
    ): Triple<Pair<Model, HeaderMetadata>, List<Resource>, Report> {
        val report = Report(
            messageId,
            status,
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
461
            generalFailureMessage = generalFailureMessage,
462
463
464
465
466
            digitalObjectMessage = digitalObjectMessage,
            thumbnailMessage = thumbnailMessage
        )
        return updateRecord(Triple(value.first, value.second, report), status = status)
    }
Sebastian Schüpbach's avatar
Sebastian Schüpbach committed
467
}