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

com.github.dynamicextensionsalfresco.controlpanel.BundleHelper.kt Maven / Gradle / Ivy

Go to download

Adds an OSGi container to alfresco repository supporting dynamic code reloading, classpath isolation and a bunch of other useful features

There is a newer version: 3.1.0
Show newest version
package com.github.dynamicextensionsalfresco.controlpanel

import aQute.bnd.osgi.Analyzer
import com.github.dynamicextensionsalfresco.event.EventListener
import com.github.dynamicextensionsalfresco.event.events.SpringContextException
import com.github.dynamicextensionsalfresco.info
import com.github.dynamicextensionsalfresco.osgi.BundleDependencies
import com.github.dynamicextensionsalfresco.osgi.ManifestUtils
import com.github.dynamicextensionsalfresco.osgi.RepositoryStoreService
import com.github.dynamicextensionsalfresco.osgi.isActive
import com.springsource.util.osgi.manifest.BundleManifest
import com.springsource.util.osgi.manifest.BundleManifestFactory
import org.alfresco.model.ContentModel
import org.alfresco.repo.content.MimetypeMap
import org.alfresco.service.cmr.model.FileFolderService
import org.alfresco.service.cmr.repository.ContentService
import org.alfresco.service.cmr.repository.NodeRef
import org.alfresco.service.cmr.repository.NodeService
import org.alfresco.service.namespace.QName
import org.osgi.framework.*
import org.osgi.framework.wiring.FrameworkWiring
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.extensions.surf.util.Content
import org.springframework.extensions.webscripts.DeclarativeRegistry
import org.springframework.extensions.webscripts.servlet.FormData.FormField
import org.springframework.stereotype.Component
import org.springframework.util.StringUtils
import java.io.*
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.LinkedBlockingDeque
import java.util.concurrent.TimeUnit
import java.util.jar.JarFile
import java.util.regex.Pattern
import javax.annotation.PostConstruct
import javax.annotation.Resource
import org.springframework.extensions.webscripts.Container as WSContainer

/**
 * Helper for working with [Bundle]s.

 * @author Laurens Fridael
 */
@Component
public open class BundleHelper constructor() : EventListener, FrameworkListener {

    @Autowired
    var bundleContext: BundleContext?=null;
    @Autowired
    var repositoryStoreService: RepositoryStoreService?=null;
    @Autowired
    var fileFolderService: FileFolderService?=null;
    @Autowired
    var contentService: ContentService?=null;
    @Autowired
    var nodeService: NodeService?=null;
    @Autowired
    @Resource(name = "webscripts.container")
    var webScriptsContainer: WSContainer?=null

    constructor(bundleContext: BundleContext,
                repositoryStoreService: RepositoryStoreService,
                fileFolderService: FileFolderService,
                contentService: ContentService,
                nodeService: NodeService,webScriptsContainer:WSContainer):this(){
        this.bundleContext = bundleContext;
        this.repositoryStoreService = repositoryStoreService;
        this.fileFolderService = fileFolderService;
        this.contentService = contentService;
        this.nodeService = nodeService;
        this.webScriptsContainer = webScriptsContainer;

    }


    open val bundleRepositoryLocation: String
        get() = repositoryStoreService!!.bundleRepositoryLocation

    /**
     * async backlog of bundles to start when the package-admin is done refreshing dependencies
     */
    private val bundlesToStart = ConcurrentLinkedQueue()

    /**
     * installBundle operations block on this queue until either an error or successful install is reported
     */
    private val installResults = LinkedBlockingDeque()

    /* Main operations */

    @PostConstruct
    public fun registerEventListeners() {
        // get notified of Spring context start failures
        bundleContext!!.registerService(EventListener::class.java, this, null)
    }

    /**
     * Obtains the [Bundle]s that comprise the core framework.
     */
    val frameworkBundles: List
        get() = bundleContext!!.bundles
            .filter { !isDynamicExtension(it) }

    /**
     * Obtains the [Bundle]s that comprise the core framework.
     */
    val extensionBundles: List
        get() = bundleContext!!.bundles
            .filter { isDynamicExtension(it) }

    /**
     * Obtains the [Bundle] for the given id.

     * @param id BundleId
     * *
     * @return The matching [Bundle] or null if no match could be found.
     */
    public fun getBundle(id: Long): Bundle {
        return bundleContext!!.getBundle(id)
    }

    /**
     * Installs an uploaded file as a bundle in the repository.
     *
     *
     * This implementation first saves the upload to a temporary file. It then attempts to install the file as a bundle.
     * If this succeeds, it saves the bundle in the repository.

     * @param file form field
     * *
     * @return installed Bundle
     * *
     * @throws IOException
     * *
     * @throws BundleException
     */
    public fun installBundleInRepository(file: FormField): Bundle? {
        val tempFile = saveToTempFile(file.inputStream)
        return doInstallBundleInRepository(tempFile, file.filename)
    }

    /**
     * Installs a bundle using the given [Content] and filename.

     * @param content uploaded content
     * *
     * @return installted Bundle
     * *
     * @throws IOException
     * *
     * @throws BundleException
     */
    public fun installBundleInRepository(content: Content): Bundle? {
        val tempFile = saveToTempFile(content.inputStream)
        return doInstallBundleInRepository(tempFile, null)
    }

    @Throws(BundleException::class)
    public open fun uninstallAndDeleteBundle(bundle: Bundle): NodeRef? {
        var matchingNode: NodeRef? = null
        val matcher = Pattern.compile("/Company Home(/.+)+/(.+\\.jar)$").matcher(bundle.location)
        if (matcher.matches()) {
            val filename = matcher.group(2)
            val bundleFolder = repositoryStoreService!!.getBundleFolder(false)
            if (bundleFolder != null) {
                val file = fileFolderService!!.searchSimple(bundleFolder, filename)
                if (file != null) {
                    val properties = emptyMap()
                    nodeService!!.addAspect(file, ContentModel.ASPECT_TEMPORARY, properties)
                    nodeService!!.deleteNode(file)
                    matchingNode = file
                    bundle.uninstall()
                }
            }
        }

        return matchingNode
    }

    public fun getAllServices(): Array>? {
        return bundleContext!!.getAllServiceReferences(null, null)
    }

    fun  getService(service: Class): T? {
        val serviceReference = bundleContext!!.getServiceReference(service)
        if (serviceReference != null) {
            return bundleContext!!.getService(serviceReference)
        } else {
            return null
        }
    }

    /* Utility operations */

    protected open val frameworkWiring: FrameworkWiring
        get() = bundleContext!!.getBundle(0).adapt(FrameworkWiring::class.java)

    fun doInstallBundleInRepository(tempFile: File, fileName: String?): Bundle? {
        var jarToInstall = tempFile

        installResults.clear()

        try {
            var identifier: BundleIdentifier? = getBundleIdentifier(jarToInstall)
            if (identifier == null) {
                jarToInstall = wrapPlainJar(tempFile, fileName)
                identifier = getBundleIdentifier(jarToInstall)
                if (identifier == null) {
                    throw BundleException("Could not generate Bundle filename. Make sure the content is an OSGi bundle.")
                }
                val symbolicName = identifier.symbolicName
                logger.info { "Wrapped plain jar as a OSGi bundle: $symbolicName." }
            }
            val filename = identifier.toJarFilename()
            val location = generateRepositoryLocation(filename)
            var bundle: Bundle? = bundleContext!!.getBundle(location)

            // a classpath bundle cannot be replaced in a persistent way, so we only do temporary updates here
            var classpathBundle = false
            if (bundle == null) {
                bundle = findBundleBySymbolicName(identifier)
                if (bundle != null) {
                    val deletedNode = uninstallAndDeleteBundle(bundle)
                    if (deletedNode != null) {
                        logger.warn("Deleted existing repository bundle {} with an identical Symbolic name: {}.", deletedNode, identifier.symbolicName)
                        bundle = null
                    } else {
                        classpathBundle = true
                    }
                }
            }

            val inputStream = createStreamForFile(jarToInstall)
            if (bundle != null) {
                // we stop and delay restarting the bundle, as otherwise, the refresh would cause 2 immediate restarts,
                bundle.stop()
                bundle.update(inputStream)

                val wiring = frameworkWiring

                val bundleSet = setOf(bundle)

                // resolve to synchronously assert dependencies are in order
                wiring.resolveBundles(bundleSet)

                if (isFragmentBundle(bundle) == false) {
                    bundlesToStart.offer(bundle)
                    val dependantBundles = wiring.getDependencyClosure(bundleSet).filter { it.isActive }
                    val dependantBundlesSorted = BundleDependencies.sortByDependencies(dependantBundles)

                    for (dependendant in dependantBundlesSorted.reversed()) {
                        dependendant.stop()
                    }
                    for (dependendant in dependantBundlesSorted) {
                        bundlesToStart.offer(dependendant)
                    }

                    // async operation
                    wiring.refreshBundles(bundleSet, this)
                } else {
                    return bundle
                }
            } else {
                bundle = bundleContext!!.installBundle(location, inputStream)
                if (isFragmentBundle(bundle) == false) {
                    bundle!!.start()
                    installResults.add(InstallResult(null))
                }
            }

            if (bundle != null) {
                if (!classpathBundle) {
                    val manifest = createBundleManifest(bundle)
                    saveBundleInRepository(jarToInstall, filename, manifest)
                } else {
                    logger.warn("Temporarily updated classpath bundle: {}, update will be reverted after restart.", bundle.symbolicName)
                }

                try {
                    evaluateInstallationResult(installResults.poll(1, TimeUnit.MINUTES))
                } catch (tx: InterruptedException) {
                    logger.warn("Timed out waiting for an installation result", tx)
                }

                resetWebScriptsCache()
            }

            return bundle
        } finally {
            jarToInstall.delete()
        }
    }

    protected open fun createBundleManifest(bundle: Bundle): BundleManifest {
        return BundleManifestFactory.createBundleManifest(bundle.headers)
    }

    protected open fun createStreamForFile(file: File): InputStream {
        return FileInputStream(file)
    }

    private fun wrapPlainJar(tempFile: File, fileName: String?): File {
        try {
            val jar = JarFile(tempFile)

            val analyzer = Analyzer()
            val manifestVersion = ManifestUtils.getImplementationVersion(jar)
            if (manifestVersion != null) {
                analyzer.bundleVersion = manifestVersion
            }
            var name = ManifestUtils.getImplementationTitle(jar)
            if (name == null) {
                if (fileName == null) {
                    return tempFile
                } else {
                    name = fileName.replaceFirst("^(.+)\\.\\w+$".toRegex(), "$1")
                }
            }
            analyzer.setBundleSymbolicName(name)

            analyzer.setJar(tempFile)
            analyzer.setImportPackage("*;resolution:=optional")
            analyzer.setExportPackage("*")
            analyzer.analyze()
            val manifest = analyzer.calcManifest()
            analyzer.jar.manifest = manifest
            val wrappedTempFile = File.createTempFile("wrapped", ".jar")
            analyzer.save(wrappedTempFile, true)
            return wrappedTempFile
        } catch (e: Exception) {
            logger.warn("Failed to wrap plain $tempFile jar using bnd.", e)
            return tempFile
        }
    }

    protected open fun findBundleBySymbolicName(identifier: BundleIdentifier): Bundle? {
        return bundleContext!!.bundles
                .firstOrNull{ it?.symbolicName == identifier.symbolicName }
    }

    protected fun saveToTempFile(data: InputStream): File {
        val tempFile = File.createTempFile("dynamic-extensions-bundle", null)
        tempFile.deleteOnExit()
        data.copyTo(FileOutputStream(tempFile))
        return tempFile
    }

    protected open fun getBundleIdentifier(tempFile: File): BundleIdentifier? {
        var identifier: BundleIdentifier? = null
        val jarFile = JarFile(tempFile)
        try {
            val manifest = jarFile.manifest
            val attributes = manifest.mainAttributes
            val symbolicName = attributes.getValue(Constants.BUNDLE_SYMBOLICNAME)
            val version = attributes.getValue(Constants.BUNDLE_VERSION)
            if (StringUtils.hasText(symbolicName) && StringUtils.hasText(version)) {
                identifier = BundleIdentifier.fromSymbolicNameAndVersion(symbolicName, version)
            }
            return identifier
        } finally {
            jarFile.close()
        }
    }

    protected open fun saveBundleInRepository(file: File, filename: String, manifest: BundleManifest) {
        val bundleFolder = repositoryStoreService!!.getBundleFolder(true)
        var nodeRef: NodeRef? = fileFolderService!!.searchSimple(bundleFolder, filename)
        if (nodeRef == null) {
            nodeRef = fileFolderService!!.create(bundleFolder, filename, ContentModel.TYPE_CONTENT).nodeRef
        }
        val title = "%s %s".format(manifest.bundleName, manifest.bundleVersion)
        nodeService!!.setProperty(nodeRef, ContentModel.PROP_TITLE, title)

        // disable indexing
        nodeService!!.setProperty(nodeRef, ContentModel.PROP_DESCRIPTION, manifest.bundleDescription)
        nodeService!!.addAspect(nodeRef, ContentModel.ASPECT_INDEX_CONTROL, mapOf(
                ContentModel.PROP_IS_INDEXED to false
        ));

        val writer = contentService!!.getWriter(nodeRef, ContentModel.PROP_CONTENT, true)
        writer.mimetype = MimetypeMap.MIMETYPE_ZIP
        writer.putContent(createStreamForFile(file))
    }

    protected fun generateRepositoryLocation(filename: String): String {
        return "%s/%s".format(bundleRepositoryLocation, filename)
    }

    protected open fun isFragmentBundle(bundle: Bundle): Boolean {
        return bundle.headers.get(Constants.FRAGMENT_HOST) != null
    }

    /**
     * The DeclarativeRegistry caches 404 results, which can hide new webscript deployments.
     * Unfortunately there is no public API for resetting this cache.
     */
    @Suppress("UNCHECKED_CAST")
    protected open fun resetWebScriptsCache() {
        val registry = webScriptsContainer!!.registry
        if (registry is DeclarativeRegistry) {
            try {
                val cacheField = DeclarativeRegistry::class.java.getDeclaredField("uriIndexCache")
                if (!cacheField.isAccessible) {
                    cacheField.isAccessible = true
                }
                val cache = cacheField.get(registry) as MutableMap
                cache.clear()
            } catch (e: Exception) {
                logger.error("failed to reset webscript cache", e)
            }
        }
    }

    override fun onEvent(event: SpringContextException) {
        installResults.add(InstallResult(event.exception))
    }

    override val supportedEventTypes: Array> = arrayOf(SpringContextException::class.java)

    override fun frameworkEvent(event: FrameworkEvent) {
        if (event.type == FrameworkEvent.PACKAGES_REFRESHED) {
            // start any bundles that were recently updated after the PackageAdmin has refreshed (restarted) any dependencies
            var bundle = bundlesToStart.poll()
            while (bundle != null) {
                try {
                    bundle.start()
                    installResults.add(InstallResult(null))
                } catch (bx: BundleException) {
                    installResults.add(InstallResult(bx))
                }
                bundle = bundlesToStart.poll()
            }
        }
    }

    private class InstallResult(public val exception: Exception?)

    companion object {
        private val logger = LoggerFactory.getLogger(BundleHelper::class.java)

        private val ALFRESCO_DYNAMIC_EXTENSION_HEADER = "Alfresco-Dynamic-Extension"

        /**
         * Tests if the given bundle contains a Dynamic Extension.
         *
         *
         * This implementation looks if the bundle header `Alfresco-Dynamic-Extension` equals the String "true".
         */
        public fun isDynamicExtension(bundle: Bundle): Boolean {
            return "true" == bundle.headers.get(ALFRESCO_DYNAMIC_EXTENSION_HEADER)
        }

        private fun evaluateInstallationResult(installResult: InstallResult?) {
            if (installResult != null) {
                if (installResult.exception is RuntimeException) {
                    throw installResult.exception
                } else if (installResult.exception is BundleException) {
                    throw installResult.exception
                }
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy