/*--------------------------------------------------------------------------------------------- * Copyright (c) Red Hat, Inc. All rights reserved. * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; import { JSONSchema, JSONSchemaMap, JSONSchemaRef } from '../jsonSchema07'; import { SchemaRequestService, WorkspaceContextService, PromiseConstructor, Thenable, } from '../yamlLanguageService'; import { UnresolvedSchema, ResolvedSchema, JSONSchemaService, SchemaDependencies, ISchemaContributions, } from 'vscode-json-languageservice/lib/umd/services/jsonSchemaService'; import * as nls from 'vscode-nls'; import { convertSimple2RegExpPattern } from '../utils/strings'; const localize = nls.loadMessageBundle(); export declare type CustomSchemaProvider = (uri: string) => Thenable; export class FilePatternAssociation { private schemas: string[]; private patternRegExp: RegExp; constructor(pattern: string) { try { this.patternRegExp = new RegExp( convertSimple2RegExpPattern(pattern) + '$' ); } catch (e) { // invalid pattern this.patternRegExp = null; } this.schemas = []; } public addSchema(id: string) { this.schemas.push(id); } public matchesPattern(fileName: string): boolean { return this.patternRegExp && this.patternRegExp.test(fileName); } public getSchemas() { return this.schemas; } } export class YAMLSchemaService extends JSONSchemaService { private customSchemaProvider: CustomSchemaProvider | undefined; private filePatternAssociations: FilePatternAssociation[]; private contextService: WorkspaceContextService; constructor( requestService: SchemaRequestService, contextService?: WorkspaceContextService, promiseConstructor?: PromiseConstructor ) { super(requestService, contextService, promiseConstructor); this.customSchemaProvider = undefined; } registerCustomSchemaProvider(customSchemaProvider: CustomSchemaProvider) { this.customSchemaProvider = customSchemaProvider; } //tslint:disable public resolveSchemaContent( schemaToResolve: UnresolvedSchema, schemaURL: string, dependencies: SchemaDependencies ): Thenable { let resolveErrors: string[] = schemaToResolve.errors.slice(0); let schema = schemaToResolve.schema; let contextService = this.contextService; let findSection = (schema: JSONSchema, path: string): any => { if (!path) { return schema; } let current: any = schema; if (path[0] === '/') { path = path.substr(1); } path.split('/').some(part => { current = current[part]; return !current; }); return current; }; let merge = ( target: JSONSchema, sourceRoot: JSONSchema, sourceURI: string, path: string ): void => { let section = findSection(sourceRoot, path); if (section) { for (let key in section) { if (section.hasOwnProperty(key) && !target.hasOwnProperty(key)) { target[key] = section[key]; } } } else { resolveErrors.push( localize( 'json.schema.invalidref', "$ref '{0}' in '{1}' can not be resolved.", path, sourceURI ) ); } }; let resolveExternalLink = ( node: JSONSchema, uri: string, linkPath: string, parentSchemaURL: string, parentSchemaDependencies: SchemaDependencies ): Thenable => { if (contextService && !/^\w+:\/\/.*/.test(uri)) { uri = contextService.resolveRelativePath(uri, parentSchemaURL); } uri = this.normalizeId(uri); const referencedHandle = this.getOrAddSchemaHandle(uri); return referencedHandle.getUnresolvedSchema().then(unresolvedSchema => { parentSchemaDependencies[uri] = true; if (unresolvedSchema.errors.length) { let loc = linkPath ? uri + '#' + linkPath : uri; resolveErrors.push( localize( 'json.schema.problemloadingref', "Problems loading reference '{0}': {1}", loc, unresolvedSchema.errors[0] ) ); } merge(node, unresolvedSchema.schema, uri, linkPath); return resolveRefs( node, unresolvedSchema.schema, uri, referencedHandle.dependencies ); }); }; let resolveRefs = ( node: JSONSchema, parentSchema: JSONSchema, parentSchemaURL: string, parentSchemaDependencies: SchemaDependencies ): Thenable => { if (!node || typeof node !== 'object') { return Promise.resolve(null); } let toWalk: JSONSchema[] = [node]; let seen: JSONSchema[] = []; let openPromises: Thenable[] = []; let collectEntries = (...entries: JSONSchemaRef[]) => { for (let entry of entries) { if (typeof entry === 'object') { toWalk.push(entry); } } }; let collectMapEntries = (...maps: JSONSchemaMap[]) => { for (let map of maps) { if (typeof map === 'object') { for (let key in map) { let entry = map[key]; if (typeof entry === 'object') { toWalk.push(entry); } } } } }; let collectArrayEntries = (...arrays: JSONSchemaRef[][]) => { for (let array of arrays) { if (Array.isArray(array)) { for (let entry of array) { if (typeof entry === 'object') { toWalk.push(entry); } } } } }; let handleRef = (next: JSONSchema) => { let seenRefs = []; while (next.$ref) { const ref = next.$ref; let segments = ref.split('#', 2); delete next.$ref; if (segments[0].length > 0) { openPromises.push( resolveExternalLink( next, segments[0], segments[1], parentSchemaURL, parentSchemaDependencies ) ); return; } else { if (seenRefs.indexOf(ref) === -1) { merge(next, parentSchema, parentSchemaURL, segments[1]); // can set next.$ref again, use seenRefs to avoid circle seenRefs.push(ref); } } } collectEntries( next.items, next.additionalProperties, next.not, next.contains, next.propertyNames, next.if, next.then, next.else ); collectMapEntries( next.definitions, next.properties, next.patternProperties, next.dependencies ); collectArrayEntries( next.anyOf, next.allOf, next.oneOf, next.items, next.schemaSequence ); }; while (toWalk.length) { let next = toWalk.pop(); if (seen.indexOf(next) >= 0) { continue; } seen.push(next); handleRef(next); } return Promise.all(openPromises); }; return resolveRefs(schema, schema, schemaURL, dependencies).then( _ => new ResolvedSchema(schema, resolveErrors) ); } //tslint:enable public getSchemaForResource( resource: string, doc = undefined ): Thenable { const resolveSchema = () => { const seen: { [schemaId: string]: boolean } = Object.create(null); const schemas: string[] = []; for (const entry of this.filePatternAssociations) { if (entry.matchesPattern(resource)) { for (const schemaId of entry.getSchemas()) { if (!seen[schemaId]) { schemas.push(schemaId); seen[schemaId] = true; } } } } if (schemas.length > 0) { return super .createCombinedSchema(resource, schemas) .getResolvedSchema() .then(schema => { if ( schema.schema && schema.schema.schemaSequence && schema.schema.schemaSequence[doc.currentDocIndex] ) { return new ResolvedSchema( schema.schema.schemaSequence[doc.currentDocIndex] ); } return schema; }); } return Promise.resolve(null); }; if (this.customSchemaProvider) { return this.customSchemaProvider(resource) .then(schemaUri => { if (!schemaUri) { return resolveSchema(); } return this.loadSchema(schemaUri).then(unsolvedSchema => this.resolveSchemaContent(unsolvedSchema, schemaUri, []).then( schema => { if ( schema.schema && schema.schema.schemaSequence && schema.schema.schemaSequence[doc.currentDocIndex] ) { return new ResolvedSchema( schema.schema.schemaSequence[doc.currentDocIndex] ); } return schema; } ) ); }) .then( schema => schema, err => resolveSchema() ); } else { return resolveSchema(); } } /** * Everything below here is needed because we're importing from vscode-json-languageservice umd and we need * to provide a wrapper around the javascript methods we are calling since they have no type */ normalizeId(id: string) { return super.normalizeId(id); } getOrAddSchemaHandle(id: string, unresolvedSchemaContent?: JSONSchema) { return super.getOrAddSchemaHandle(id, unresolvedSchemaContent); } // tslint:disable-next-line: no-any loadSchema(schemaUri: string): Thenable { return super.loadSchema(schemaUri); } registerExternalSchema( uri: string, filePatterns?: string[], unresolvedSchema?: JSONSchema ) { return super.registerExternalSchema(uri, filePatterns, unresolvedSchema); } clearExternalSchemas(): void { super.clearExternalSchemas(); } setSchemaContributions(schemaContributions: ISchemaContributions): void { super.setSchemaContributions(schemaContributions); } getRegisteredSchemaIds(filter?: (scheme: any) => boolean): string[] { return super.getRegisteredSchemaIds(filter); } getResolvedSchema(schemaId: string): Thenable { return super.getResolvedSchema(schemaId); } onResourceChange(uri: string): boolean { return super.onResourceChange(uri); } }