import * as vscode from "vscode" ; import { VSCExpress } from "vscode-express" ; import { BadRequestError } from "../common/badRequestError" ; import { ColorizedChannel } from "../common/colorizedChannel" ; import { Configuration } from "../common/configuration" ; import { Constants } from "../common/constants" ; import { CredentialStore } from "../common/credentialStore" ; import { ProcessError } from "../common/processError" ; import { UserCancelledError } from "../common/userCancelledError" ; import { Utility } from "../common/utility" ; import { DeviceModelManager , ModelType } from "../deviceModel/deviceModelManager" ; import { DigitalTwinConstants } from "../intelliSense/digitalTwinConstants" ; import { ChoiceType , MessageType , UI } from "../view/ui" ; import { UIConstants } from "../view/uiConstants" ; import { ModelRepositoryClient } from "./modelRepositoryClient" ; import { ModelRepositoryConnection } from "./modelRepositoryConnection" ; import { GetResult , SearchResult } from "./modelRepositoryInterface" ; import { TelemetryContext } from "../../../../telemetry" ; /**
 * Repository type
 */ export enum RepositoryType { Public = "Public repository" , Company = "Company repository" } /**
 * Repository info
 */ export interface RepositoryInfo { hostname : string ; apiVersion : string ; repositoryId ? : string ; accessToken ? : string ; } /**
 * Model file info
 */ export interface ModelFileInfo { id : string ; type : ModelType ; filePath : string ; } /**
 * Submit options
 */ interface SubmitOptions { overwrite : boolean ; } /**
 * Model repository manager
 */ export class ModelRepositoryManager { param publicRepository private static async createRepositoryInfo ( publicRepository ) { if ( publicRepository ) { const url = Configuration . getProperty < string > ( Constants . PUBLIC_REPOSITORY_URL ) ; if ( ! url ) { throw new Error ( Constants . PUBLIC_REPOSITORY_URL_NOT_FOUND_MSG ) ; } return { hostname : Utility . enforceHttps ( url ) , apiVersion : Constants . MODEL_REPOSITORY_API_VERSION } ; } else { const connectionString = await CredentialStore . get ( Constants . MODEL_REPOSITORY_CONNECTION_KEY ) ; if ( ! connectionString ) { throw new Error ( Constants . CONNECTION_STRING_NOT_FOUND_MSG ) ; } return ModelRepositoryManager . getCompanyRepositoryInfo ( connectionString ) ; } } /**
     * get available repository info, company repository is prior to public repository
     */ private static async getAvailableRepositoryInfo ( ) { const repoInfos = [ ] ; const connectionString = await CredentialStore . get ( Constants . MODEL_REPOSITORY_CONNECTION_KEY ) ; if ( connectionString ) { repoInfos . push ( ModelRepositoryManager . getCompanyRepositoryInfo ( connectionString ) ) ; } repoInfos . push ( await ModelRepositoryManager . createRepositoryInfo ( true ) ) ; return repoInfos ; } /**
     * set up company model repository connection
     */ private static async setupConnection ( ) { let newConnection = false ; let connectionString = await CredentialStore . get ( Constants . MODEL_REPOSITORY_CONNECTION_KEY ) ; if ( ! connectionString ) { connectionString = await UI . inputConnectionString ( UIConstants . INPUT_REPOSITORY_CONNECTION_STRING_LABEL ) ; newConnection = true ; } const repoInfo = ModelRepositoryManager . getCompanyRepositoryInfo ( connectionString ) ; await ModelRepositoryClient . searchModel ( repoInfo , ModelType . Interface , Constants . EMPTY_STRING , 1 , null ) ; if ( newConnection ) { await CredentialStore . set ( Constants . MODEL_REPOSITORY_CONNECTION_KEY , connectionString ) ; } } param connectionString private static getCompanyRepositoryInfo ( connectionString ) { const connection = ModelRepositoryConnection . parse ( connectionString ) ; return { hostname : Utility . enforceHttps ( connection . hostName ) , apiVersion : Constants . MODEL_REPOSITORY_API_VERSION , repositoryId : connection . repositoryId , accessToken : connection . generateAccessToken ( ) } ; } param modelIds private static validateModelIds ( modelIds ) { if ( ! modelIds || ! modelIds . length ) { throw new BadRequestError ( `modelIds ${ Constants . NOT_EMPTY_MSG }` ) ; } } private readonly express : VSCExpress ; private readonly component : string ; constructor ( context , filePath , private readonly outputChannel ) { this . express = new VSCExpress ( context , filePath ) ; this . component = Constants . MODEL_REPOSITORY_COMPONENT ; } /**
     * sign in model repository
     */ async signIn ( ) { const items = [ { label : RepositoryType . Public } , { label : RepositoryType . Company } ] ; const selected = await UI . showQuickPick ( UIConstants . SELECT_REPOSITORY_LABEL , items ) ; const operation = `Connect to ${ selected . label }` ; this . outputChannel . start ( operation , this . component ) ; if ( selected . label === RepositoryType . Company ) { try { await ModelRepositoryManager . setupConnection ( ) ; } catch ( error ) { if ( error instanceof UserCancelledError ) { throw error ; } else { throw new ProcessError ( operation , error , this . component ) ; } } } const uri = selected . label === RepositoryType . Company ? Constants . COMPANY_REPOSITORY_PAGE : Constants . PUBLIC_REPOSITORY_PAGE ; this . express . open ( uri , UIConstants . MODEL_REPOSITORY_TITLE , vscode . ViewColumn . Two , { retainContextWhenHidden : true , enableScripts : true } ) ; UI . showNotification ( MessageType . Info , ColorizedChannel . formatMessage ( operation ) ) ; this . outputChannel . end ( operation , this . component ) ; } /**
     * sign out model repository
     */ async signOut ( ) { const operation = "Sign out company repository" ; this . outputChannel . start ( operation , this . component ) ; await CredentialStore . delete ( Constants . MODEL_REPOSITORY_CONNECTION_KEY ) ; if ( this . express ) { this . express . close ( Constants . COMPANY_REPOSITORY_PAGE ) ; } UI . showNotification ( MessageType . Info , ColorizedChannel . formatMessage ( operation ) ) ; this . outputChannel . end ( operation , this . component ) ; } param telemetryContext async submitFiles ( telemetryContext ) { const files = await UI . selectModelFiles ( UIConstants . SELECT_MODELS_LABEL ) ; if ( ! files . length ) { return ; } await UI . ensureFilesSaved ( UIConstants . SAVE_FILE_CHANGE_LABEL , files ) ; try { await ModelRepositoryManager . setupConnection ( ) ; } catch ( error ) { if ( error instanceof UserCancelledError ) { throw error ; } else { throw new ProcessError ( `Connect to ${ RepositoryType . Company }` , error , this . component ) ; } } try { const repoInfo = await ModelRepositoryManager . createRepositoryInfo ( false ) ; await this . doSubmitLoopSilently ( repoInfo , files , telemetryContext ) ; } catch ( error ) { const operation = `Submit models to ${ RepositoryType . Company }` ; throw new ProcessError ( operation , error , this . component ) ; } } param type param publicRepository param keyword param pageSize param continuationToken async searchModel ( type , publicRepository , keyword = Constants . EMPTY_STRING , pageSize = Constants . DEFAULT_PAGE_SIZE , continuationToken = null ) { if ( pageSize <= 0 ) { throw new BadRequestError ( "pageSize should be greater than 0" ) ; } const showOutput = keyword ? true : false ; const operation = `Search ${ type } by keyword "${ keyword }" from ${ publicRepository ? RepositoryType . Public : RepositoryType . Company }` ; if ( showOutput ) { this . outputChannel . start ( operation , this . component ) ; } let result ; try { const repoInfo = await ModelRepositoryManager . createRepositoryInfo ( publicRepository ) ; result = await ModelRepositoryClient . searchModel ( repoInfo , type , keyword , pageSize , continuationToken ) ; } catch ( error ) { throw new ProcessError ( operation , error , this . component ) ; } if ( showOutput ) { this . outputChannel . end ( operation , this . component ) ; } return result ; } param publicRepository param modelIds async deleteModels ( publicRepository , modelIds ) { if ( publicRepository ) { throw new BadRequestError ( `${ RepositoryType . Public } not support delete operation` ) ; } ModelRepositoryManager . validateModelIds ( modelIds ) ; try { const repoInfo = await ModelRepositoryManager . createRepositoryInfo ( publicRepository ) ; await this . doDeleteLoopSilently ( repoInfo , modelIds ) ; } catch ( error ) { const operation = `Delete models from ${ RepositoryType . Company }` ; throw new ProcessError ( operation , error , this . component ) ; } } param publicRepository param modelIds async downloadModels ( publicRepository , modelIds ) { ModelRepositoryManager . validateModelIds ( modelIds ) ; const folder = await UI . selectRootFolder ( UIConstants . SELECT_ROOT_FOLDER_LABEL ) ; try { const repoInfo = await ModelRepositoryManager . createRepositoryInfo ( publicRepository ) ; await this . doDownloadLoopSilently ( [ repoInfo ] , modelIds , folder ) ; } catch ( error ) { const operation = `Download models from ${ publicRepository ? RepositoryType . Public : RepositoryType . Company }` ; throw new ProcessError ( operation , error , this . component ) ; } } param folder param capabilityModelFile async downloadDependentInterface ( folder , capabilityModelFile ) { if ( ! folder || ! capabilityModelFile ) { throw new BadRequestError ( `folder and capabilityModelFile ${ Constants . NOT_EMPTY_MSG }` ) ; } const content = await Utility . getJsonContent ( capabilityModelFile ) ; const implementedInterface = content [ DigitalTwinConstants . IMPLEMENTS ] ; if ( ! implementedInterface || ! implementedInterface . length ) { throw new BadRequestError ( "no implemented interface found in capability model" ) ; } const repoInfos = await ModelRepositoryManager . getAvailableRepositoryInfo ( ) ; const fileInfos = await UI . findModelFiles ( ModelType . Interface ) ; const exist = new Set < string > ( fileInfos . map ( f => f . id ) ) ; let schema ; let found ; let message ; for ( const item of implementedInterface ) { schema = item [ DigitalTwinConstants . SCHEMA ] ; if ( typeof schema !== "string" || exist . has ( schema ) ) { continue ; } found = await this . doDownloadModel ( repoInfos , schema , folder ) ; if ( ! found ) { message = `interface ${ schema } not found` ; if ( repoInfos . length === 1 ) { message = `${ message }. ${ Constants . NEED_OPEN_COMPANY_REPOSITORY_MSG }` ; } throw new BadRequestError ( message ) ; } } } param repoInfos param modelIds param folder private async doDownloadLoopSilently ( repoInfos , modelIds , folder ) { for ( const modelId of modelIds ) { const operation = `Download model by id ${ modelId }` ; this . outputChannel . start ( operation , this . component ) ; try { await this . doDownloadModel ( repoInfos , modelId , folder ) ; this . outputChannel . end ( operation , this . component ) ; } catch ( error ) { this . outputChannel . error ( operation , this . component , error ) ; } } } param repoInfos param modelId param folder private async doDownloadModel ( repoInfos , modelId , folder ) { let result ; for ( const repoInfo of repoInfos ) { try { result = await ModelRepositoryClient . getModel ( repoInfo , modelId , true ) ; break ; } catch ( error ) { if ( error . statusCode === Constants . NOT_FOUND_CODE ) { this . outputChannel . warn ( `Model ${ modelId } is not found from ${ repoInfo . hostname }` ) ; } else { this . outputChannel . error ( `Fail to get model ${ modelId } from ${ repoInfo . hostname }, statusCode: ${ error . statusCode }` ) ; } } } if ( result ) { await Utility . createModelFile ( folder , result . modelId , result . content ) ; return true ; } return false ; } param repoInfo param modelIds private async doDeleteLoopSilently ( repoInfo , modelIds ) { for ( const modelId of modelIds ) { const operation = `Delete model by id ${ modelId }` ; this . outputChannel . start ( operation , this . component ) ; try { await ModelRepositoryClient . deleteModel ( repoInfo , modelId ) ; this . outputChannel . end ( operation , this . component ) ; } catch ( error ) { this . outputChannel . error ( operation , this . component , error ) ; } } } param repoInfo param files param telemetryContext private async doSubmitLoopSilently ( repoInfo , files , telemetryContext ) { const usageData = new Map < ModelType , string [ ] > ( ) ; const options = { overwrite : false } ; for ( const file of files ) { const operation = `Submit file ${ file }` ; this . outputChannel . start ( operation , this . component ) ; try { await this . doSubmitModel ( repoInfo , file , options , usageData ) ; this . outputChannel . end ( operation , this . component ) ; } catch ( error ) { this . outputChannel . error ( operation , this . component , error ) ; } } this . setTelemetryOfSubmitFiles ( telemetryContext , usageData , files . length ) ; } param repoInfo param filePath param options param usageData private async doSubmitModel ( repoInfo , filePath , option , usageData ) { const content = await Utility . getJsonContent ( filePath ) ; const modelId = content [ DigitalTwinConstants . ID ] ; const modelType = DeviceModelManager . convertToModelType ( content [ DigitalTwinConstants . TYPE ] ) ; let result ; try { result = await ModelRepositoryClient . getModel ( repoInfo , modelId , true ) ; } catch ( error ) { if ( error . statusCode !== Constants . NOT_FOUND_CODE ) { throw error ; } } if ( result ) { if ( ! option . overwrite ) { const message = `Model ${ modelId } already exist, ${ UIConstants . ASK_TO_OVERWRITE_MSG }` ; const choice = await vscode . window . showWarningMessage ( message , ChoiceType . All , ChoiceType . Yes , ChoiceType . No ) ; if ( ! choice || choice === ChoiceType . No ) { this . outputChannel . warn ( `Skip overwrite model ${ modelId }` ) ; return ; } else if ( choice === ChoiceType . All ) { option . overwrite = true ; } } } await ModelRepositoryClient . updateModel ( repoInfo , modelId , content ) ; let modelIds = usageData . get ( modelType ) ; if ( ! modelIds ) { modelIds = [ ] ; usageData . set ( modelType , modelIds ) ; } modelIds . push ( modelId ) ; } param telemetryContext param usageData param totalCount private setTelemetryOfSubmitFiles ( telemetryContext , usageData , totalCount ) { let succeedCount = 0 ; let hashId ; for ( const [ key , value ] of usageData ) { succeedCount += value . length ; hashId = value . map ( id => Utility . hash ( id ) ) . join ( Constants . DEFAULT_SEPARATOR ) ; switch ( key ) { case ModelType . Interface : telemetryContext . properties . interfaceId = hashId ; break ; case ModelType . CapabilityModel : telemetryContext . properties . capabilityModelId = hashId ; break ; default : } } telemetryContext . measurements . totalCount = totalCount ; telemetryContext . measurements . succeedCount = succeedCount ; } }