Skip to content

Commit

Permalink
better transport selection, FM creation fixes, small fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
marcellourbani committed Dec 3, 2018
1 parent 6e750f1 commit b518a04
Show file tree
Hide file tree
Showing 9 changed files with 109 additions and 54 deletions.
5 changes: 5 additions & 0 deletions src/abap/AbapClassInclude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AbapObject> {
if (this.parent) {
await this.parent.loadMetadata(connection)
Expand Down
4 changes: 4 additions & 0 deletions src/abap/AbapObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,10 @@ export class AbapObject {
return this.sapguiOnly ? ".txt" : ".abap"
}

getActivationSubject(): AbapObject {
return this
}

async getChildren(
connection: AdtConnection
): Promise<Array<AbapNodeComponentByCategory>> {
Expand Down
47 changes: 40 additions & 7 deletions src/adt/AdtConnection.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<Function> = []
private _clone?: AdtConnection

constructor(name: string, url: string, username: string, password: string) {
this.name = name
Expand All @@ -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<AdtConnection> {
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
}
Expand Down Expand Up @@ -57,7 +80,17 @@ export class AdtConnection {
): Promise<request.Response> {
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(
Expand Down Expand Up @@ -87,9 +120,9 @@ export class AdtConnection {
return new Promise<Response>((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))
Expand Down
27 changes: 11 additions & 16 deletions src/adt/AdtExceptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>
// namespace: string
// type: string
// message: string
// localizedMessage: string
// properties: Map<string, string>
get typeID(): Symbol {
return TYPEID
}

constructor(
namespace: string,
type: string,
message: string,
localizedMessage: string,
properties: Map<string, string>
public type: string,
public message: string,
public namespace?: string,
public localizedMessage?: string,
public properties?: Map<string, string>
) {
super()
this.namespace = namespace
this.type = type
this.message = message
this.localizedMessage = localizedMessage
this.properties = properties
}

static async fromXml(xml: string): Promise<AdtException> {
Expand All @@ -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()
)
Expand Down
13 changes: 7 additions & 6 deletions src/adt/AdtObjectActivator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand All @@ -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
Expand All @@ -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)
}
}
Expand Down
46 changes: 34 additions & 12 deletions src/adt/AdtTransports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface TransportHeader {
AS4TEXT: string
CLIENT: string
}

interface TransportInfo {
PGMID: string
OBJECT: string
Expand All @@ -31,6 +32,7 @@ interface TransportInfo {
RECORDING: string
EXISTING_REQ_ONLY: string
TRANSPORTS: TransportHeader[]
LOCKS: TransportHeader[]
}
interface ValidateTransportMessage {
SEVERITY: string
Expand All @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -92,6 +110,10 @@ export async function selectTransport(
conn: AdtConnection
): Promise<string> {
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([
Expand Down
18 changes: 6 additions & 12 deletions src/adt/create/AdtObjectCreator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ export class AdtObjectCreator {
private async guessOrSelectObjectType(
hierarchy: AbapNode[]
): Promise<CreatableObjectType | undefined> {
//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)
Expand Down Expand Up @@ -121,7 +120,6 @@ export class AdtObjectCreator {
objType: CreatableObjectType,
objDetails: NewObjectConfig
): Promise<string> {
//TODO: no request for temp packages
const uri = this.server.connection.createUri(objType.getPath(objDetails))
return selectTransport(uri, objDetails.devclass, this.server.connection)
}
Expand All @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion src/adt/create/AdtObjectTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}">
<adtcore:containerRef adtcore:name="${config.parentName}"
adtcore:type="${this._parentType}"
adtcore:uri="${this.getBasePath(config)}"/>
Expand Down
1 change: 1 addition & 0 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down

0 comments on commit b518a04

Please sign in to comment.