From b518a04925519c3b22d671da8d84a75f8694d3dd Mon Sep 17 00:00:00 2001 From: marcello Date: Mon, 3 Dec 2018 09:13:46 +0000 Subject: [PATCH] better transport selection, FM creation fixes, small fixes --- src/abap/AbapClassInclude.ts | 5 ++++ src/abap/AbapObject.ts | 4 +++ src/adt/AdtConnection.ts | 47 +++++++++++++++++++++++++----- src/adt/AdtExceptions.ts | 27 +++++++---------- src/adt/AdtObjectActivator.ts | 13 +++++---- src/adt/AdtTransports.ts | 46 +++++++++++++++++++++-------- src/adt/create/AdtObjectCreator.ts | 18 ++++-------- src/adt/create/AdtObjectTypes.ts | 2 +- src/commands.ts | 1 + 9 files changed, 109 insertions(+), 54 deletions(-) diff --git a/src/abap/AbapClassInclude.ts b/src/abap/AbapClassInclude.ts index da0daf3..87bd8bc 100644 --- a/src/abap/AbapClassInclude.ts +++ b/src/abap/AbapClassInclude.ts @@ -27,6 +27,11 @@ export class AbapClassInclude extends AbapObject { query: `version=${this.metaData.version}` }) } + + getActivationSubject(): AbapObject { + return this.parent || this + } + async loadMetadata(connection: AdtConnection): Promise { if (this.parent) { await this.parent.loadMetadata(connection) diff --git a/src/abap/AbapObject.ts b/src/abap/AbapObject.ts index 80963ae..6986755 100644 --- a/src/abap/AbapObject.ts +++ b/src/abap/AbapObject.ts @@ -236,6 +236,10 @@ export class AbapObject { return this.sapguiOnly ? ".txt" : ".abap" } + getActivationSubject(): AbapObject { + return this + } + async getChildren( connection: AdtConnection ): Promise> { diff --git a/src/adt/AdtConnection.ts b/src/adt/AdtConnection.ts index c4edf59..aa059f4 100644 --- a/src/adt/AdtConnection.ts +++ b/src/adt/AdtConnection.ts @@ -1,9 +1,11 @@ import * as request from "request" import { Uri } from "vscode" import { RemoteConfig } from "../config" -import { AdtException, AdtHttpException } from "./AdtExceptions" +import { AdtException, AdtHttpException, isAdtException } from "./AdtExceptions" import { Response } from "request" +const CSRF_EXPIRED = "CSRF_EXPIRED" +const FETCH_CSRF_TOKEN = "fetch" enum ConnStatus { new, active, @@ -14,11 +16,12 @@ export class AdtConnection { readonly url: string readonly username: string readonly password: string - //TODO: hack for object creation, needs proper session support and backend cache invalidation + //TODO: proper session support stateful = true - private _csrftoken: string = "fetch" + private _csrftoken: string = FETCH_CSRF_TOKEN private _status: ConnStatus = ConnStatus.new private _listeners: Array = [] + private _clone?: AdtConnection constructor(name: string, url: string, username: string, password: string) { this.name = name @@ -27,6 +30,26 @@ export class AdtConnection { this.password = password } + /** + * get a stateless clone of the original connection + * + * some calls, like object creation must be done in a separate connection + * to prevent leaving dirty data in function groups, which makes other calls fail + */ + async getStatelessClone(): Promise { + if (!this._clone) { + this._clone = new AdtConnection( + this.name + "_clone", + this.url, + this.username, + this.password + ) + this._clone.stateful = false + } + await this._clone.connect() + return this._clone + } + isActive(): boolean { return this._status === ConnStatus.active } @@ -57,7 +80,17 @@ export class AdtConnection { ): Promise { if (this._status !== ConnStatus.active) await this.waitReady() const path = uri.query ? uri.path + "?" + uri.query : uri.path - return this.myrequest(path, method, config) + try { + return await this.myrequest(path, method, config) + } catch (e) { + if (isAdtException(e) && e.type === CSRF_EXPIRED) { + //Token expired, try getting a new one + // only retry once! + this._csrftoken = FETCH_CSRF_TOKEN + await this.connect() + return this.myrequest(path, method, config) + } else throw e + } } private myrequest( @@ -87,9 +120,9 @@ export class AdtConnection { return new Promise((resolve, reject) => { request(urlOptions, async (error, response, body) => { if (error) reject(error) - //TODO:support 304 non modified? Should only happen if I send a header like - //If-None-Match: 201811061933580005ZDEMO_CALENDAR - else if (response.statusCode < 300) resolve(response) + else if (response.statusCode < 400) resolve(response) + else if (response.statusCode === 403 && body.match(/CSRF.*failed/)) + reject(new AdtException(CSRF_EXPIRED, "")) else try { reject(await AdtException.fromXml(body)) diff --git a/src/adt/AdtExceptions.ts b/src/adt/AdtExceptions.ts index dc95fff..3875a61 100644 --- a/src/adt/AdtExceptions.ts +++ b/src/adt/AdtExceptions.ts @@ -3,28 +3,23 @@ import { Response } from "request" const TYPEID = Symbol() export class AdtException extends Error { - namespace: string - type: string - message: string - localizedMessage: string - properties: Map + // namespace: string + // type: string + // message: string + // localizedMessage: string + // properties: Map get typeID(): Symbol { return TYPEID } constructor( - namespace: string, - type: string, - message: string, - localizedMessage: string, - properties: Map + public type: string, + public message: string, + public namespace?: string, + public localizedMessage?: string, + public properties?: Map ) { super() - this.namespace = namespace - this.type = type - this.message = message - this.localizedMessage = localizedMessage - this.properties = properties } static async fromXml(xml: string): Promise { @@ -34,9 +29,9 @@ export class AdtException extends Error { const type = getFieldAttribute("type", "id", root) const values = recxml2js(root) return new AdtException( - namespace, type, values.message, + namespace, values.localizedMessage, new Map() ) diff --git a/src/adt/AdtObjectActivator.ts b/src/adt/AdtObjectActivator.ts index 4cbaf78..2fcbe47 100644 --- a/src/adt/AdtObjectActivator.ts +++ b/src/adt/AdtObjectActivator.ts @@ -84,14 +84,15 @@ export class AdtObjectActivator { return "" } - async activate(obj: AbapObject) { + async activate(object: AbapObject) { + const inactive = object.getActivationSubject() let message = "" try { - let retval = await this._activate(obj) + let retval = await this._activate(inactive) if (retval) { if (isString(retval)) message = retval else { - retval = await this._activate(obj, retval) + retval = await this._activate(inactive, retval) if (isString(retval)) message = retval else throw new Error("Unexpected activation error") } @@ -100,8 +101,8 @@ export class AdtObjectActivator { if (isAdtException(e)) { switch (e.type) { case "invalidMainProgram": - const mainProg = await this.selectMain(obj) - const res = await this._activate(obj, mainProg) + const mainProg = await this.selectMain(inactive) + const res = await this._activate(inactive, mainProg) if (isString(res)) message = res else throw new Error("Unexpected activation error") break @@ -113,7 +114,7 @@ export class AdtObjectActivator { if (message) window.showErrorMessage(message) else { //activation successful, update the status. By the book we should check if it's set by this object first... - await obj.loadMetadata(this.connection) + await inactive.loadMetadata(this.connection) commands.executeCommand("setContext", "abapfs:objectInactive", false) } } diff --git a/src/adt/AdtTransports.ts b/src/adt/AdtTransports.ts index bd45646..1adf9b3 100644 --- a/src/adt/AdtTransports.ts +++ b/src/adt/AdtTransports.ts @@ -15,6 +15,7 @@ interface TransportHeader { AS4TEXT: string CLIENT: string } + interface TransportInfo { PGMID: string OBJECT: string @@ -31,6 +32,7 @@ interface TransportInfo { RECORDING: string EXISTING_REQ_ONLY: string TRANSPORTS: TransportHeader[] + LOCKS: TransportHeader[] } interface ValidateTransportMessage { SEVERITY: string @@ -42,6 +44,31 @@ interface ValidateTransportMessage { function throwMessage(msg: ValidateTransportMessage) { throw new Error(`${msg.TEXT} (${msg.SEVERITY}${msg.MSGNR}(${msg.ARBGB}))`) } +function extracttLocks(raw: any): TransportHeader[] { + let locks: TransportHeader[] | undefined + try { + locks = getNode( + "asx:abap/asx:values/DATA/LOCKS/CTS_OBJECT_LOCK/LOCK_HOLDER/REQ_HEADER", + mapWith(recxml2js), + raw + ) + } catch (e) {} + return locks || [] +} +function extracttTransports(raw: any): TransportHeader[] { + let transports: TransportHeader[] | undefined + try { + transports = getNode( + "asx:abap/asx:values/DATA/REQUESTS/CTS_REQUEST", + mapWith(getNode("REQ_HEADER")), + flat, + mapWith(recxml2js), + raw + ) + } catch (e) {} + return transports || [] +} + export async function getTransportCandidates( objContentUri: Uri, devClass: string, @@ -71,19 +98,10 @@ export async function getTransportCandidates( ) as ValidateTransportMessage[] messages.filter(x => x.SEVERITY === "E").map(throwMessage) } - const RAWTRANSPORTS = getNode("asx:abap/asx:values/DATA/REQUESTS", rawdata) - const TRANSPORTS = - RAWTRANSPORTS && RAWTRANSPORTS[0] - ? getNode( - "CTS_REQUEST", - mapWith(getNode("REQ_HEADER")), - flat, - mapWith(recxml2js), - RAWTRANSPORTS - ) - : [] + const LOCKS = extracttLocks(rawdata) + const TRANSPORTS = extracttTransports(rawdata) - return { ...header, TRANSPORTS } + return { ...header, TRANSPORTS, LOCKS } } export async function selectTransport( @@ -92,6 +110,10 @@ export async function selectTransport( conn: AdtConnection ): Promise { const ti = await getTransportCandidates(objContentUri, devClass, conn) + //if I have a lock return the locking transport + // will probably be a task but should be fine + if (ti.LOCKS.length > 0) return ti.LOCKS[0].TRKORR + if (ti.DLVUNIT === "LOCAL") return "" const CREATENEW = "Create a new transport" let selection = await window.showQuickPick([ diff --git a/src/adt/create/AdtObjectCreator.ts b/src/adt/create/AdtObjectCreator.ts index 2d98f6e..b811c25 100644 --- a/src/adt/create/AdtObjectCreator.ts +++ b/src/adt/create/AdtObjectCreator.ts @@ -74,7 +74,6 @@ export class AdtObjectCreator { private async guessOrSelectObjectType( hierarchy: AbapNode[] ): Promise { - //TODO: guess from URI const base = hierarchy[0] //if I picked the root node,a direct descendent or a package just ask the user to select any object type // if not, for abap nodes pick child objetc types (if any) @@ -121,7 +120,6 @@ export class AdtObjectCreator { objType: CreatableObjectType, objDetails: NewObjectConfig ): Promise { - //TODO: no request for temp packages const uri = this.server.connection.createUri(objType.getPath(objDetails)) return selectTransport(uri, objDetails.devclass, this.server.connection) } @@ -137,18 +135,14 @@ export class AdtObjectCreator { objDetails: NewObjectConfig, request: string ) { - const uri = this.server.connection.createUri( - objType.getBasePath(objDetails) - ) + const conn = await this.server.connection.getStatelessClone() + const uri = conn + .createUri(objType.getBasePath(objDetails)) + .with({ query: request && `corrNr=${request}` }) let body = objType.getCreatePayload(objDetails) - const query = request ? `corrNr=${request}` : "" - //TODO hack for cache invalidation, need to find a proper solution - this.server.connection.stateful = false - let response = await this.server.connection.request(uri, "POST", { - body, - query + let response = await conn.request(uri, "POST", { + body }) - this.server.connection.stateful = true return response } private async askInput( diff --git a/src/adt/create/AdtObjectTypes.ts b/src/adt/create/AdtObjectTypes.ts index 4c22440..9d3ce4c 100644 --- a/src/adt/create/AdtObjectTypes.ts +++ b/src/adt/create/AdtObjectTypes.ts @@ -107,7 +107,7 @@ class FGObjectType extends CreatableObjectType { xmlns:adtcore="http://www.sap.com/adt/core" adtcore:description="${config.description}" adtcore:name="${config.name}" adtcore:type="${this.type}" - adtcore:responsible="${config.responsible}> + adtcore:responsible="${config.responsible}"> diff --git a/src/commands.ts b/src/commands.ts index 04e7f3e..adfe76e 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -20,6 +20,7 @@ export async function activateCurrent(selector: Uri) { try { const server = fromUri(selector) const obj = await server.findAbapObject(selector) + if (!obj.metaData) await obj.loadMetadata(server.connection) await server.activate(obj) } catch (e) { window.showErrorMessage(e.toString())