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

com.jetbrains.plugin.structure.edu.EduPluginManager.kt Maven / Gradle / Ivy

Go to download

Library for parsing JetBrains Edu plugins. Can be used to verify that plugin complies with JetBrains Marketplace requirements.

The newest version!
/*
 * Copyright 2000-2020 JetBrains s.r.o. and other contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
 */

package com.jetbrains.plugin.structure.edu

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.jetbrains.plugin.structure.base.decompress.DecompressorSizeLimitExceededException
import com.jetbrains.plugin.structure.base.plugin.*
import com.jetbrains.plugin.structure.base.problems.*
import com.jetbrains.plugin.structure.base.utils.*
import com.jetbrains.plugin.structure.edu.bean.EduPluginDescriptor
import com.jetbrains.plugin.structure.edu.problems.createIncorrectEduPluginFile
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths

class EduPluginManager private constructor(private val extractDirectory: Path) : PluginManager {
  companion object {
    const val DESCRIPTOR_NAME = "course.json"
    const val COURSE_ICON_NAME = "courseIcon.svg"

    private val LOG: Logger = LoggerFactory.getLogger(EduPluginManager::class.java)

    fun createManager(
      extractDirectory: Path = Paths.get(Settings.EXTRACT_DIRECTORY.get())
    ): EduPluginManager {
      extractDirectory.createDir()
      return EduPluginManager(extractDirectory)
    }
  }

  override fun createPlugin(pluginFile: Path): PluginCreationResult {
    require(pluginFile.exists()) { "Plugin file $pluginFile does not exist" }
    return when {
      pluginFile.isZip() -> loadDescriptorFromZip(pluginFile)
      else -> PluginCreationFail(createIncorrectEduPluginFile(pluginFile.simpleName))
    }
  }

  private fun loadDescriptorFromZip(pluginFile: Path): PluginCreationResult {
    val sizeLimit = Settings.EDU_PLUGIN_SIZE_LIMIT.getAsLong()
    if (Files.size(pluginFile) > sizeLimit) {
      return PluginCreationFail(UnableToExtractZip())
    }

    val tempDirectory = Files.createTempDirectory(extractDirectory, "plugin_")
    return try {
      extractZip(pluginFile, tempDirectory, sizeLimit)
      loadPluginInfoFromDirectory(tempDirectory)
    } catch (e: DecompressorSizeLimitExceededException) {
      return PluginCreationFail(PluginFileSizeIsTooLarge(e.sizeLimit))
    } finally {
      tempDirectory.deleteLogged()
    }
  }

  private fun loadPluginInfoFromDirectory(pluginDirectory: Path): PluginCreationResult {
    val descriptorFile = pluginDirectory.resolve(DESCRIPTOR_NAME)
    if (!descriptorFile.exists()) {
      return PluginCreationFail(PluginDescriptorIsNotFound(DESCRIPTOR_NAME))
    }
    val descriptorContent = descriptorFile.readText()
    val mapper = jacksonObjectMapper()
    val descriptor = mapper.readValue(descriptorContent, EduPluginDescriptor::class.java)
    val icon = loadIconFromDir(pluginDirectory)
    return createPlugin(descriptor, icon)
  }

  private fun loadIconFromDir(pluginDirectory: Path): PluginIcon? {
    val iconFile = pluginDirectory.resolve(COURSE_ICON_NAME)
    if (iconFile.exists()) {
      val iconContent = Files.readAllBytes(iconFile)
      return PluginIcon(IconTheme.DEFAULT, iconContent, iconFile.fileName.toString())
    }
    return null
  }

  private fun createPlugin(descriptor: EduPluginDescriptor, icon: PluginIcon?): PluginCreationResult {
    try {
      val beanValidationResult = validateEduPluginBean(descriptor)
      if (beanValidationResult.any { it.level == PluginProblem.Level.ERROR }) {
        return PluginCreationFail(beanValidationResult)
      }
      val plugin = with(descriptor) {
        /*
          This is a workaround on complex, strange and outdated logic about xml id generation. Long story short:
            1. There was no xml id for edu plugins, so we need to generate it by our self. The original generation alg was
               "${this.title}_${this.vendor?.name}_$programmingLanguage".
            2. Then, it appears that authors should be able to change the `title`. To do so, edu team introduced
               `generated_edu_id` field in course json. The idea is to generate it the same way as described in (1) in
               the edu plugin and only after that allow author to change the `title`.
            3. Now, it appears that `programmingLanguage` could contain the programming language, and it's version divided
               by space. Edu also want to split `programmingLanguage` to `programmingLanguageId` and `programmingLanguageVersion`.
               Also, `programmingLanguageVersion` could be changed in future versions of the course, which means it
               could also affect xml id generation.

          So, the plan is:
            1. For older versions of the course structure (with `programmingLanguage`) we will get rid of the programming
               language version in xml id. I'll also remove the version from xml id in the database. From now on
               `programmingLanguage` is deprecated with estimated period of 6 edu plugin releases (half of a year).
            2. For newer versions of the course structure (with `programmingLanguageId` and `programmingLanguageVersion`)
               we will use only `programmingLanguageId` in xml id.
            3. We also decided to remove the part with generation xml id and to use `generated_edu_id` instead for every
               plugin with an estimated period of 6 edu plugin releases.
         */
        val xmlIdSuffix = this.programmingLanguageId ?: this.programmingLanguage?.substringBefore(" ")
        val pluginId = this.pluginId ?: "${this.title}_${this.vendor?.name}_$xmlIdSuffix"

        EduPlugin(
          pluginName = this.title,
          description = this.summary,
          vendor = this.vendor?.name,
          vendorUrl = this.vendor?.vendorUrl,
          vendorEmail = this.vendor?.vendorEmail,
          descriptorVersion = this.descriptorVersion,
          pluginVersion = this.pluginVersion,
          language = this.language,
          programmingLanguage = programmingLanguage,
          programmingLanguageId = programmingLanguageId,
          programmingLanguageVersion = this.programmingLanguageVersion,
          environment = this.environment,
          isPrivate = this.isPrivate ?: false,
          eduStat = EduStat.fromDescriptor(this),
          icons = if (icon != null) listOf(icon) else emptyList(),
          pluginId = pluginId
        )
      }
      return PluginCreationSuccess(plugin, beanValidationResult)
    } catch (e: Exception) {
      e.rethrowIfInterrupted()
      LOG.info("Unable to read plugin descriptor $DESCRIPTOR_NAME", e)
      return PluginCreationFail(UnableToReadDescriptor(DESCRIPTOR_NAME, e.localizedMessage))
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy