diff --git a/src/abap/AbapObject.ts b/src/abap/AbapObject.ts index 7d4d144..79d879a 100644 --- a/src/abap/AbapObject.ts +++ b/src/abap/AbapObject.ts @@ -31,6 +31,11 @@ export interface AbapMetaData { masterLanguage?: string masterSystem?: string } +export enum TransportStatus { + UNKNOWN, + REQUIRED, + LOCAL +} interface MainProgram { "adtcore:uri": string "adtcore:type": string @@ -43,6 +48,8 @@ export class AbapObject { readonly techName: string readonly path: string readonly expandable: boolean + lockId?: string + transport: TransportStatus | string = TransportStatus.UNKNOWN metaData?: AbapMetaData protected sapguiOnly: boolean @@ -117,15 +124,8 @@ export class AbapObject { ) } - async setContents( - connection: AdtConnection, - contents: Uint8Array - ): Promise { - if (!this.isLeaf()) throw FileSystemError.FileIsADirectory(this.vsName()) - if (this.sapguiOnly) - throw FileSystemError.FileNotFound( - `${this.name} can only be edited in SAPGUI` - ) + async lock(connection: AdtConnection) { + this.checkWritable() let contentUri = this.getContentsUri(connection) const response = await connection.request( @@ -134,17 +134,49 @@ export class AbapObject { { headers: { "X-sap-adt-sessiontype": "stateful" } } ) const lockRecord = await parsetoPromise(adtLockParser)(response.body) - const lock = encodeURIComponent(lockRecord.LOCK_HANDLE) - + this.lockId = lockRecord.LOCK_HANDLE + this.transport = + lockRecord.CORRNR || + (lockRecord.IS_LOCAL ? TransportStatus.LOCAL : TransportStatus.REQUIRED) + } + async unlock(connection: AdtConnection) { + this.checkWritable() + if (!this.lockId) return + let contentUri = this.getContentsUri(connection) await connection.request( - contentUri.with({ query: `lockHandle=${lock}` }), - "PUT", - { body: contents } + contentUri.with({ + query: `_action=UNLOCK&lockHandle=${encodeURIComponent(this.lockId)}` + }), + "POST" ) + } + + protected checkWritable() { + if (!this.isLeaf()) throw FileSystemError.FileIsADirectory(this.vsName()) + if (this.sapguiOnly) + throw FileSystemError.FileNotFound( + `${this.name} can only be edited in SAPGUI` + ) + } + + async setContents( + connection: AdtConnection, + contents: Uint8Array + ): Promise { + this.checkWritable() + let contentUri = this.getContentsUri(connection) + + const trselection = + typeof this.transport === "string" ? `&corrNr=${this.transport}` : "" await connection.request( - contentUri.with({ query: `_action=UNLOCK&lockHandle=${lock}` }), - "POST" + contentUri.with({ + query: `lockHandle=${encodeURIComponent( + this.lockId || "" + )}${trselection}` + }), + "PUT", + { body: contents } ) } diff --git a/src/abap/JSONToAbapXml.ts b/src/abap/JSONToAbapXml.ts new file mode 100644 index 0000000..fdb52f7 --- /dev/null +++ b/src/abap/JSONToAbapXml.ts @@ -0,0 +1,16 @@ +export function JSON2AbapXML(original: any, root: string = "DATA") { + //only flat objects for now, might extend later... + let inner = "" + for (const key of Object.keys(original)) + if (original[key]) + inner = `${inner}\n<${key}>${original[key] || ""}` + else inner = `${inner}\n<${key}/>` + + return ` + + <${root}> + ${inner} + + +` +} diff --git a/src/adt/AdtConnection.ts b/src/adt/AdtConnection.ts index 723cb62..96bf4b0 100644 --- a/src/adt/AdtConnection.ts +++ b/src/adt/AdtConnection.ts @@ -94,7 +94,12 @@ export class AdtConnection { }) }) } - + createUri(path: string, query: string = "") { + return Uri.parse("adt://" + this.name).with({ + path, + query + }) + } connect(): Promise { return this.myrequest("/sap/bc/adt/compatibility/graph").then( (response: request.Response) => { diff --git a/src/adt/AdtServer.ts b/src/adt/AdtServer.ts index 9f45fb9..a8bb607 100644 --- a/src/adt/AdtServer.ts +++ b/src/adt/AdtServer.ts @@ -2,8 +2,9 @@ import { AdtConnection } from "./AdtConnection" import { Uri, FileSystemError, FileType, window, commands } from "vscode" import { MetaFolder } from "../fs/MetaFolder" import { AbapObjectNode, AbapNode, isAbap } from "../fs/AbapNode" -import { AbapObject } from "../abap/AbapObject" +import { AbapObject, TransportStatus } from "../abap/AbapObject" import { getRemoteList } from "../config" +import { JSON2AbapXML } from "../abap/JSONToAbapXml" export const ADTBASEURL = "/sap/bc/adt/repository/nodestructure" // visual studio paths are hierarchic, adt ones aren't @@ -49,6 +50,34 @@ export class AdtServer { new AbapObjectNode(new AbapObject("DEVC/K", "", ADTBASEURL, "X")) ) } + + async saveFile(file: AbapNode, content: Uint8Array): Promise { + if (file.isFolder()) throw FileSystemError.FileIsADirectory() + if (!isAbap(file)) + throw FileSystemError.NoPermissions("Can only save source code") + + const conn = await this.connectionP + await file.abapObject.lock(conn) + if (file.abapObject.transport === TransportStatus.REQUIRED) { + const response = await conn.request( + conn.createUri("/sap/bc/adt/cts/transportchecks"), + "POST", + { + body: JSON2AbapXML({ URI: file.abapObject.getContentsUri(conn).path }) + } + ) + console.log(response.body) + throw new Error("transport selection not supported(yet)") + } + + await file.abapObject.setContents(conn, content) + + await file.abapObject.unlock(conn) + await file.stat(conn) + //might have a race condition with user changing editor... + commands.executeCommand("setContext", "abapfs:objectInactive", true) + } + findNode(uri: Uri): AbapNode { const parts = uriParts(uri) return parts.reduce((current: any, name) => { diff --git a/src/fs/AbapNode.ts b/src/fs/AbapNode.ts index 71f301d..4aab13e 100644 --- a/src/fs/AbapNode.ts +++ b/src/fs/AbapNode.ts @@ -122,11 +122,7 @@ export class AbapObjectNode implements FileStat, Iterable<[string, AbapNode]> { return Promise.reject(e) } } - public save(connection: AdtConnection, contents: Uint8Array) { - if (this.isFolder()) throw FileSystemError.FileIsADirectory() - //returning a promise will allow the exceptions to propagate - return this.abapObject.setContents(connection, contents) - } + public refresh(connection: AdtConnection): Promise { return this.abapObject.getChildren(connection).then(objects => { refreshObjects(this, objects) diff --git a/src/fs/FsProvider.ts b/src/fs/FsProvider.ts index ae4708d..71fad67 100644 --- a/src/fs/FsProvider.ts +++ b/src/fs/FsProvider.ts @@ -1,6 +1,6 @@ import * as vscode from "vscode" import { fromUri } from "../adt/AdtServer" -import { FileSystemError, commands } from "vscode" +import { FileSystemError } from "vscode" export class FsProvider implements vscode.FileSystemProvider { private _eventEmitter = new vscode.EventEmitter() @@ -58,12 +58,7 @@ export class FsProvider implements vscode.FileSystemProvider { "Not a real filesystem, file creation is not supported" ) if (!file) throw FileSystemError.FileNotFound(uri) - const connection = await server.connectionP - await file.save(connection, content) - //not active anymore... update the status. By the book we should check if it's set by this object first... - //TODO: move this logic somewhere else... - await this.stat(uri) - commands.executeCommand("setContext", "abapfs:objectInactive", true) + return server.saveFile(file, content) } delete( uri: vscode.Uri,