I have attachemnt in my data model, so I use FileRef but recently I had to allow to have the link in the database but keep the file in an external tools (sharepoint)
So I created new FileStorage HTTP and HTTPS to have keep the url.
import io.jmix.core.FileRef
import io.jmix.core.FileStorage
import org.springframework.core.io.ResourceLoader
import org.springframework.stereotype.Component
import java.io.IOException
import java.io.InputStream
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.time.Duration
@Component("com_ExternalHttpFileStorage")
class ExternalHttpFileStorage (private val resourceLoader: ResourceLoader) : FileStorage {
companion object {
const val NAME = "http"
}
private val httpClient: HttpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.followRedirects(HttpClient.Redirect.NORMAL)
.build()
override fun getStorageName() = NAME
override fun openStream(reference: FileRef): InputStream {
try {
val request = HttpRequest.newBuilder()
.uri(URI.create(storageName + "://" + reference.path))
.GET()
.timeout(Duration.ofSeconds(30))
.build()
val response = httpClient.send(
request,
HttpResponse.BodyHandlers.ofInputStream()
)
if (response.statusCode() in 200..299) {
return response.body()
}
throw IOException("Failed to open remote file. HTTP status: ${response.statusCode()}")
} catch (ex: Exception) {
throw RuntimeException("Error opening HTTP resource", ex)
}
}
override fun fileExists(reference: FileRef): Boolean {
return try {
val request = HttpRequest.newBuilder()
.uri(URI.create(reference.path))
.method("HEAD", HttpRequest.BodyPublishers.noBody())
.timeout(Duration.ofSeconds(5))
.build()
val response = httpClient.send(
request,
HttpResponse.BodyHandlers.discarding()
)
response.statusCode() in 200..399
} catch (ex: Exception) {
false
}
}
override fun removeFile(reference: FileRef) {
// Nothing to delete (remote resource not managed)
}
fun saveStream(uri: URI): FileRef {
return FileRef (storageName, URI(
storageName,
uri.authority,
uri.path,
uri.query,
uri.fragment
).toURL().toString().replaceFirst("$storageName://", ""),
(uri.path + (uri.fragment?.let { "#$it" } ?: "")).substringAfterLast("/"))
}
fun saveStream(fileName: String, parameters: MutableMap<String, Any> = mutableMapOf()): FileRef
= FileRef(
storageName,
fileName,
fileName.substringAfterLast("/")
)
override fun saveStream(fileName: String, inputStream: InputStream, parameters: MutableMap<String, Any>): FileRef
= saveStream(fileName, inputStream, parameters)
}
@Component("com_ExternalHttpsFileStorage")
class ExternalHttpsFileStorage (resourceLoader: ResourceLoader) : ExternalHttpFileStorage(resourceLoader) {
companion object {
const val NAME = "https"
}
override fun getStorageName() = NAME
}
But I have a problem if the link contains a blank, that I replace by %20 because FileRefConverter use static FileRef.fromString() method and this method decode url then check blank.
import io.jmix.core.FileRef
import io.jmix.core.common.util.URLEncodeUtils
import jakarta.persistence.AttributeConverter
import jakarta.persistence.Converter
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.context.annotation.Primary
import org.springframework.stereotype.Component
import java.net.URI
import java.net.URISyntaxException
@Primary
@Component
@Converter(autoApply = true)
class ComFileRefConverter : AttributeConverter<FileRef, String> {
val log: Logger = LoggerFactory.getLogger(ComFileRefConverter::class.java)
override fun convertToDatabaseColumn(attribute: FileRef?): String? {
return attribute?.toString()?.replace("%20", "")?.replace("#", "%23")
}
override fun convertToEntityAttribute(dbData: String?): FileRef? {
return try {
when {
dbData == null -> null
dbData.startsWith(ExternalHttpFileStorage.NAME+"://") -> fromString(dbData)
dbData.startsWith(ExternalHttpsFileStorage.NAME+"://") -> fromString(dbData)
else -> FileRef.fromString(dbData)
}
} catch (e: RuntimeException) {
log.warn("Cannot convert $dbData to FileRef", e)
null
}
}
fun fromString(fileRefString: String): FileRef {
val fileRefUri: URI?
try {
fileRefUri = URI(fileRefString)
} catch (e: URISyntaxException) {
throw IllegalArgumentException("Cannot convert " + fileRefString + " to FileRef", e)
}
val storageName = fileRefUri.getScheme()
val path = if (fileRefUri.getAuthority() == null)
fileRefUri.getPath()
else
fileRefUri.getAuthority() + fileRefUri.getPath()
val query = fileRefUri.getRawQuery()
// FIX authorize blank in HTTP
//require(!StringUtils.isAnyBlank(storageName, path, query)) { "Cannot convert " + fileRefString + " to FileRef" }
val params: Array<String?> = query.split("&".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val nameParamPair: Array<String?> = params[0]!!.split("=".toRegex()).toTypedArray()
require(nameParamPair.size == 2) { "Cannot convert " + fileRefString + " to FileRef" }
val fileName = URLEncodeUtils.decodeUtf8(nameParamPair[1])
if (params.size > 1) {
val paramsMap: MutableMap<String?, String?> = LinkedHashMap<String?, String?>()
for (i in 1..<params.size) {
val paramPair: Array<String?> = params[i]!!.split("=".toRegex()).toTypedArray()
require(paramPair.size == 2) { "Cannot convert " + fileRefString + " to FileRef" }
paramsMap.put(paramPair[0], URLEncodeUtils.decodeUtf8(paramPair[1]))
}
return FileRef(storageName, path, fileName, paramsMap)
}
return FileRef(storageName, path, fileName)
}
}
And I need to apply the converter on each field
@Convert(disableConversion = true, converter = ComFileRefConverter::class)
@Column(name = "FILE_", length = 1024, nullable = false)
@NotNull
var file: FileRef? = null
It’s ok in the build & run, but Studio don’t like this converter it in the liquibase step,
