All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dorkbox.util.CacheUtil.kt Maven / Gradle / Ivy

/*
 * Copyright 2023 dorkbox, llc
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package dorkbox.util

import dorkbox.os.OS.TEMP_DIR
import dorkbox.util.FileUtil.copyFile
import dorkbox.util.FileUtil.delete
import java.io.*
import java.math.BigInteger
import java.net.URL
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException

class CacheUtil(private val tempDir: String = "cache") {
    /**
     * Clears ALL saved files in the cache
     */
    fun clear() {
        // deletes all the files (recursively) in the specified location. If the directory is empty (no locked files), then the
        // directory is also deleted.
        delete(File(TEMP_DIR, tempDir))
    }

    /**
     * Checks to see if the specified file is in the cache. NULL if it is not, otherwise specifies a location on disk.
     *
     *
     * This cache is not persisted across runs.
     */
    fun check(file: File?): File? {
        if (file == null) {
            throw NullPointerException("file")
        }

        // if we already have this fileName, reuse it
        return check(file.absolutePath)
    }

    /**
     * Checks to see if the specified file is in the cache. NULL if it is not, otherwise specifies a location on disk.
     */
    fun check(fileName: String?): File? {
        if (fileName == null) {
            throw NullPointerException("fileName")
        }

        // if we already have this fileName, reuse it
        val newFile = makeCacheFile(fileName)

        // if this file already exists (via HASH), we just reuse what is saved on disk.
        return if (newFile.canRead() && newFile.isFile) {
            newFile
        } else null
    }

    /**
     * Checks to see if the specified URL is in the cache. NULL if it is not, otherwise specifies a location on disk.
     */
    fun check(fileResource: URL): File? {
        return check(fileResource.path)
    }

    /**
     * Checks to see if the specified stream (based on the hash of the input stream) is in the cache. NULL if it is not, otherwise
     * specifies a location on disk.
     */
    @Throws(IOException::class)
    fun check(fileStream: InputStream): File? {
        return check(null, fileStream)
    }

    /**
     * Checks to see if the specified name is in the cache. NULL if it is not, otherwise specifies a location on disk. If the
     * cacheName is NULL, it will use a HASH of the fileStream
     */
    @Throws(IOException::class)
    fun check(cacheName: String?, fileStream: InputStream): File? {
        // if we already have this fileName, reuse it
        val newFile = if (cacheName == null) {
            makeCacheFile(createNameAsHash(fileStream))
        } else {
            makeCacheFile(cacheName)
        }

        // if this file already exists (via HASH), we just reuse what is saved on disk.
        return if (newFile.canRead() && newFile.isFile) {
            newFile
        } else null
    }

    /**
     * Saves the name of the file in a cache, based on the file's name.
     */
    @Throws(IOException::class)
    fun save(file: File): File {
        return save(file.absolutePath, file)
    }

    /**
     * Saves the name of the file in a cache, based on the specified name. If cacheName is NULL, it will use the file's name.
     */
    @Throws(IOException::class)
    fun save(cacheName: String?, file: File): File {
        return if (cacheName == null) {
            save(file.absolutePath, file.absolutePath)
        } else {
            save(cacheName, file.absolutePath)
        }
    }

    /**
     * Saves the name of the file in a cache, based on the specified name.
     */
    @Throws(IOException::class)
    fun save(fileName: String): File {
        return save(null, fileName)
    }

    /**
     * Saves the name of the file in a cache, based on name. If cacheName is NULL, it will use the file's name.
     *
     * @return the newly create cache file, or an IOException if there were problems
     */
    @Throws(IOException::class)
    fun save(cacheName: String?, fileName: String): File {
        // if we already have this fileName, reuse it
        val newFile = if (cacheName == null) {
            makeCacheFile(fileName)
        } else {
            makeCacheFile(cacheName)
        }

        // if this file already exists (via HASH), we just reuse what is saved on disk.
        if (newFile.canRead() && newFile.isFile) {
            return newFile
        }


        // is file sitting on drive
        val iconTest = File(fileName)
        return if (iconTest.isFile) {
            if (!iconTest.canRead()) {
                throw IOException("File exists but unable to read source file $fileName")
            }

            // have to copy the resource to the cache
            copyFile(iconTest, newFile)
            newFile
        } else {
            // suck it out of a URL/Resource (with debugging if necessary)
            val systemResource = LocationResolver.getResource(fileName) ?: throw IOException("Unable to load URL resource $fileName")
            val inStream = systemResource.openStream()

            // saves the file into our temp location, uses HASH of cacheName
            makeFileViaStream(cacheName, inStream)
        }
    }

    /**
     * Saves the name of the URL in a cache, based on it's path.
     */
    @Throws(IOException::class)
    fun save(fileResource: URL): File {
        return save(null, fileResource)
    }

    /**
     * Saves the name of the URL in a cache, based on the specified name. If cacheName is NULL, it will use the URL's path.
     */
    @Throws(IOException::class)
    fun save(cacheName: String?, fileResource: URL): File {
        // if we already have this fileName, reuse it
        val newFile =  if (cacheName == null) {
            makeCacheFile(fileResource.path)
        } else {
            makeCacheFile(cacheName)
        }

        // if this file already exists (via HASH), we just reuse what is saved on disk.
        if (newFile.canRead() && newFile.isFile) {
            return newFile
        }
        val inStream = fileResource.openStream()

        // saves the file into our temp location, uses HASH of cacheName
        return makeFileViaStream(cacheName, inStream)
    }

    /**
     * This caches the data based on the HASH of the input stream.
     */
    @Throws(IOException::class)
    fun save(fileStream: InputStream?): File {
        if (fileStream == null) {
            throw NullPointerException("fileStream")
        }
        return save(null, fileStream)
    }

    /**
     * Saves the name of the file in a cache, based on the cacheName. If the cacheName is NULL, it will use a HASH of the fileStream
     * as the name.
     */
    @Throws(IOException::class)
    fun save(cacheName: String?, fileStream: InputStream): File {
        // if we already have this fileName, reuse it
        val newFile = if (cacheName == null) {
            makeCacheFile(createNameAsHash(fileStream))
        } else {
            makeCacheFile(cacheName)
        }

        // if this file already exists (via HASH), we just reuse what is saved on disk.
        return if (newFile.canRead() && newFile.isFile) {
            newFile
        } else makeFileViaStream(cacheName, fileStream)
    }

    /**
     * must be called from synchronized block!
     *
     * @param cacheName needs name+extension for the resource
     * @param resourceStream the resource to copy to a file on disk
     *
     * @return the full path of the resource copied to disk, or NULL if invalid
     */
    @Throws(IOException::class)
    private fun makeFileViaStream(cacheName: String?, resourceStream: InputStream?): File {
        if (resourceStream == null) {
            throw NullPointerException("resourceStream")
        }
        if (cacheName == null) {
            throw NullPointerException("cacheName")
        }
        val newFile = makeCacheFile(cacheName)

        // if this file already exists (via HASH), we just reuse what is saved on disk.
        if (newFile.canRead() && newFile.isFile) {
            return newFile.absoluteFile
        }
        var outStream: OutputStream? = null
        try {
            var read: Int
            val buffer = ByteArray(2048)
            outStream = FileOutputStream(newFile)
            while (resourceStream.read(buffer).also { read = it } > 0) {
                outStream.write(buffer, 0, read)
            }
        } catch (e: IOException) {
            // Send up exception
            val message = "Unable to copy '" + cacheName + "' to temporary location: '" + newFile.absolutePath + "'"
            throw IOException(message, e)
        } finally {
            try {
                resourceStream.close()
            } catch (ignored: Exception) {
            }
            try {
                outStream?.close()
            } catch (ignored: Exception) {
            }
        }

        //get the name of the new file
        return newFile.absoluteFile
    }

    /**
     * @param cacheName the name of the file to use in the cache. This file name can use invalid file name characters
     *
     * @return the file on disk represented by the file name
     */
    fun create(cacheName: String): File {
        return makeCacheFile(cacheName)
    }

    // creates the file that will be cached. It may, or may not already exist
    // must be called from synchronized block!
    // never returns null
    private fun makeCacheFile(cacheName: String): File {
        val saveDir = File(TEMP_DIR, tempDir)

        // can be wimpy, only one at a time
        val hash = hashName(cacheName)
        var extension = Sys.getExtension(cacheName)
        if (extension.isEmpty()) {
            extension = "cache"
        }
        val newFile = File(saveDir, "$hash.$extension").absoluteFile
        // make whatever dirs we need to.
        newFile.parentFile.mkdirs()
        return newFile
    }

    companion object {
        /**
         * Gets the version number.
         */
        val version = Sys.version

        private val digestLocal = ThreadLocal.withInitial {
            try {
                return@withInitial MessageDigest.getInstance("SHA1")
            } catch (e: NoSuchAlgorithmException) {
                throw RuntimeException("Unable to initialize hash algorithm. SHA1 digest doesn't exist?!? (This should not happen")
            }
        }

        fun clear(tempDir: String) {
            CacheUtil(tempDir).clear()
        }

        // hashed name to prevent invalid file names from being used
        private fun hashName(name: String): String {
            // figure out the fileName
            val bytes = name.toByteArray(StandardCharsets.UTF_8)
            val digest = digestLocal.get()
            digest.reset()
            digest.update(bytes)

            // convert to alpha-numeric. see https://stackoverflow.com/questions/29183818/why-use-tostring32-and-not-tostring36
            return BigInteger(1, digest.digest()).toString(32).uppercase()
        }

        // this is if we DO NOT have a file name. We hash the resourceStream bytes to base the name on that. The extension will be ".cache"
        @Throws(IOException::class)
        fun createNameAsHash(resourceStream: InputStream): String {
            val digest = digestLocal.get()
            digest.reset()
            return try {
                // we have to set the cache name based on the hash of the input stream ONLY...
                val outStream = ByteArrayOutputStream(4096) // will resize if necessary
                var read: Int
                val buffer = ByteArray(2048)
                while (resourceStream.read(buffer).also { read = it } > 0) {
                    digest.update(buffer, 0, read)
                    outStream.write(buffer, 0, read)
                }

                // convert to alpha-numeric. see https://stackoverflow.com/questions/29183818/why-use-tostring32-and-not-tostring36
                BigInteger(1, digest.digest()).toString(32).uppercase() + ".cache"
            } catch (e: IOException) {
                // Send up exception
                val message = "Unable to copy InputStream to memory."
                throw IOException(message, e)
            } finally {
                try {
                    resourceStream.close()
                } catch (ignored: Exception) {
                }
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy