Merge "fix oauth code"
[ccsdk/features.git] / sdnr / wt-odlux / odlux / apps / configurationApp / src / utilities / viewEngineHelper.ts
1 import { storeService } from '../../../../framework/src/services/storeService';
2 import { WhenAST, WhenTokenType } from '../yang/whenParser';
3
4 import {
5   ViewSpecification,
6   ViewElement,
7   isViewElementReference,
8   isViewElementList,
9   isViewElementObjectOrList,
10   isViewElementRpc,
11   isViewElementChoice,
12   ViewElementChoiceCase,
13   isViewElementObject,
14 } from '../models/uiModels';
15
16 import { Module } from '../models/yang';
17
18 import { restService } from '../services/restServices';
19
20 export type HttpResult = {
21   status: number;
22   message?: string | undefined;
23   data: {
24     [key: string]: any;
25   } | null | undefined;
26 };
27
28 export const checkResponseCode = (restResult: HttpResult) =>{
29   //403 gets handled by the framework from now on
30   return restResult.status !== 403 && ( restResult.status < 200 || restResult.status > 299);
31 };
32
33 export const resolveVPath = (current: string, vPath: string): string => {
34   if (vPath.startsWith('/')) {
35     return vPath;
36   }
37   const parts = current.split('/');
38   const vPathParts = vPath.split('/');
39   for (const part of vPathParts) {
40     if (part === '.') {
41       continue;
42     } else if (part === '..') {
43       parts.pop();
44     } else {
45       parts.push(part);
46     }
47   }
48   return parts.join('/');
49 };
50
51 export const splitVPath = (vPath: string, vPathParser : RegExp): [string, (string | undefined | null)][] => {
52   const pathParts: [string, (string | undefined | null)][] = [];
53   let partMatch: RegExpExecArray | null;
54   if (vPath) do {
55     partMatch = vPathParser.exec(vPath);
56     if (partMatch) {
57       pathParts.push([partMatch[1], partMatch[2] || (partMatch[0].includes('[]') ? null : undefined)]);
58     }
59   } while (partMatch);
60   return pathParts;
61 };
62
63 const derivedFrom = (vPath: string, when: WhenAST, viewData: any, includeSelf = false) => {
64   if (when.args?.length !== 2) {
65     throw new Error('derived-from or derived-from-or-self requires 2 arguments.');
66   }
67   const [arg1, arg2] = when.args;
68   if (arg1.type !== WhenTokenType.IDENTIFIER || arg2.type !== WhenTokenType.STRING) {
69     throw new Error('derived-from or derived-from-or-self requires first argument IDENTIFIER and second argument STRING.');
70   }
71
72   if (!storeService.applicationStore) {
73     throw new Error('storeService.applicationStore is not defined.');
74   }
75
76   const pathParts = splitVPath(arg1.value as string || '', /(?:(?:([^\/\:]+):)?([^\/]+))/g); 
77   const referenceValueParts = /(?:(?:([^\/\:]+):)?([^\/]+))/g.exec(arg2.value as string || ''); 
78
79   if (!pathParts || !referenceValueParts || pathParts.length === 0 || referenceValueParts.length === 0) {
80     throw new Error('derived-from or derived-from-or-self requires first argument PATH and second argument IDENTITY.');
81   }
82
83   if (pathParts[0][1]?.startsWith('..') || pathParts[0][1]?.startsWith('/')) {
84     throw new Error('derived-from or derived-from-or-self currently only supports relative paths.');
85   }
86
87   const { configuration: { deviceDescription: { modules } } } = storeService.applicationStore.state;
88   const dataValue = pathParts.reduce((acc, [ns, prop]) => {
89     if (prop === '.') {
90       return acc;
91     } 
92     if (acc && prop) {
93       const moduleName = ns && (Object.values(modules).find((m: Module) => m.prefix === ns) || Object.values(modules).find((m: Module) => m.name === ns))?.name;
94       return (moduleName) ? acc[`${moduleName}:${prop}`] ||  acc[prop] : acc[prop];
95     }
96     return undefined;
97   }, viewData);
98
99   let dataValueParts = dataValue && /(?:(?:([^\/\:]+):)?([^\/]+))/g.exec(dataValue);
100   if (!dataValueParts || dataValueParts.length < 2) {
101     throw new Error(`derived-from or derived-from-or-self value referenced by first argument [${arg1.value}] not found.`);
102   }
103   let [, dataValueNs, dataValueProp] = dataValueParts;
104   let dataValueModule: Module = dataValueNs && (Object.values(modules).find((m: Module) => m.name === dataValueNs));
105   let dataValueIdentity = dataValueModule && dataValueModule.identities && (Object.values(dataValueModule.identities).find((i) => i.label === dataValueProp));
106
107   if (!dataValueIdentity) {
108     throw new Error(`derived-from or derived-from-or-self identity [${dataValue}] referenced by first argument [${arg1.value}] not found.`);
109   }
110
111   const [, referenceValueNs, referenceValueProp] = referenceValueParts;
112   const referenceValueModule = referenceValueNs && (Object.values(modules).find((m: Module) => m.prefix === referenceValueNs));
113   const referenceValueIdentity = referenceValueModule && referenceValueModule.identities && (Object.values(referenceValueModule.identities).find((i) => i.label === referenceValueProp));
114
115   if (!referenceValueIdentity) {
116     throw new Error(`derived-from or derived-from-or-self identity [${arg2.value}] referenced by second argument not found.`);
117   }
118
119   let result = includeSelf && (referenceValueIdentity === dataValueIdentity);
120   while (dataValueIdentity && dataValueIdentity.base && !result) {
121     dataValueParts = dataValue && /(?:(?:([^\/\:]+):)?([^\/]+))/g.exec(dataValueIdentity.base);
122     const [, innerDataValueNs, innerDataValueProp] = dataValueParts;
123     dataValueModule = innerDataValueNs && (Object.values(modules).find((m: Module) => m.prefix === innerDataValueNs)) || dataValueModule;
124     dataValueIdentity = dataValueModule && dataValueModule.identities && (Object.values(dataValueModule.identities).find((i) => i.label === innerDataValueProp)) ;
125     result = (referenceValueIdentity === dataValueIdentity);
126   }
127
128   return result;
129 };
130
131 const evaluateWhen = async (vPath: string, when: WhenAST, viewData: any): Promise<boolean> => {
132   switch (when.type) {
133     case WhenTokenType.FUNCTION:
134       switch (when.name) {
135         case 'derived-from-or-self':
136           return derivedFrom(vPath, when, viewData, true);
137         case 'derived-from':
138           return derivedFrom(vPath, when, viewData, false);
139         default:
140           throw new Error(`Unknown function ${when.name}`);
141       }
142     case WhenTokenType.AND:
143       return !when.left || !when.right || (await evaluateWhen(vPath, when.left, viewData) && await evaluateWhen(vPath, when.right, viewData));
144     case WhenTokenType.OR:
145       return !when.left || !when.right || (await evaluateWhen(vPath, when.left, viewData) || await evaluateWhen(vPath, when.right, viewData));
146     case WhenTokenType.NOT:
147       return !when.right || ! await evaluateWhen(vPath, when.right, viewData);
148     case WhenTokenType.EXPRESSION:
149       return !(when.value && typeof when.value !== 'string') || await evaluateWhen(vPath, when.value, viewData);
150   }   
151   return true;
152 };
153
154 export const getReferencedDataList = async (refPath: string, dataPath: string, modules: { [name: string]: Module }, views: ViewSpecification[]) => {
155   const pathParts = splitVPath(refPath, /(?:(?:([^\/\:]+):)?([^\/]+))/g);  // 1 = opt: namespace / 2 = property
156   const defaultNS = pathParts[0][0];
157   let referencedModule = modules[defaultNS];
158
159   let dataMember: string;
160   let view: ViewSpecification;
161   let currentNS: string | null = null;
162   let dataUrls = [dataPath];
163   let data: any;
164
165   for (let i = 0; i < pathParts.length; ++i) {
166     const [pathPartNS, pathPart] = pathParts[i];
167     const namespace = pathPartNS != null ? (currentNS = pathPartNS) : currentNS;
168
169     const viewElement = i === 0
170       ? views[0].elements[`${referencedModule.name}:${pathPart}`]
171       : view!.elements[`${pathPart}`] || view!.elements[`${namespace}:${pathPart}`];
172
173     if (!viewElement) throw new Error(`Could not find ${pathPart} in ${refPath}`);
174     if (i < pathParts.length - 1) {
175       if (!isViewElementObjectOrList(viewElement)) {
176         throw Error(`Module: [${referencedModule.name}].[${viewElement.label}]. View element is not list or object.`);
177       }
178       view = views[+viewElement.viewId];
179       const resultingDataUrls : string[] = [];
180       if (isViewElementList(viewElement)) {
181         for (let j = 0; j < dataUrls.length; ++j) {
182           const dataUrl = dataUrls[j];
183           const restResult = (await restService.getConfigData(dataUrl));
184           if (restResult.data == null || checkResponseCode(restResult)) {
185             const message = restResult.data && restResult.data.errors && restResult.data.errors.error && restResult.data.errors.error[0] && restResult.data.errors.error[0]['error-message'] || '';
186             throw new Error(`Server Error. Status: [${restResult.status}]\n${message || restResult.message || ''}`);
187           }
188
189           let dataRaw = restResult.data[`${defaultNS}:${dataMember!}`];
190           if (dataRaw === undefined) {
191             dataRaw = restResult.data[dataMember!];
192           }
193           dataRaw = dataRaw instanceof Array
194             ? dataRaw[0]
195             : dataRaw;
196
197           data = dataRaw && dataRaw[viewElement.label] || [];
198           const keys: string[] = data.map((entry: { [key: string]: any } )=> entry[viewElement.key!]);
199           resultingDataUrls.push(...keys.map(key => `${dataUrl}/${viewElement.label.replace(/\//ig, '%2F')}=${key.replace(/\//ig, '%2F')}`));
200         }
201         dataMember = viewElement.label;
202       } else {
203         // just a member, not a list
204         const pathSegment = (i === 0
205           ? `/${referencedModule.name}:${viewElement.label.replace(/\//ig, '%2F')}`
206           : `/${viewElement.label.replace(/\//ig, '%2F')}`);
207         resultingDataUrls.push(...dataUrls.map(dataUrl => dataUrl + pathSegment));
208         dataMember = viewElement.label;
209       }
210       dataUrls = resultingDataUrls;
211     } else {
212       data = [];
213       for (let j = 0; j < dataUrls.length; ++j) {
214         const dataUrl = dataUrls[j];
215         const restResult = (await restService.getConfigData(dataUrl));
216         if (restResult.data == null || checkResponseCode(restResult)) {
217           const message = restResult.data && restResult.data.errors && restResult.data.errors.error && restResult.data.errors.error[0] && restResult.data.errors.error[0]['error-message'] || '';
218           throw new Error(`Server Error. Status: [${restResult.status}]\n${message || restResult.message || ''}`);
219         }
220         let dataRaw = restResult.data[`${defaultNS}:${dataMember!}`];
221         if (dataRaw === undefined) {
222           dataRaw = restResult.data[dataMember!];
223         }
224         dataRaw = dataRaw instanceof Array
225           ? dataRaw[0]
226           : dataRaw;
227         data.push(dataRaw);
228       }
229       // BUG UUID ist nicht in den elements enthalten !!!!!!
230       const key = viewElement && viewElement.label || pathPart;
231       return {
232         view: view!,
233         data: data,
234         key: key,
235       };
236     }
237   }
238   return null;
239 };
240
241 export const resolveViewDescription = (defaultNS: string | null, vPath: string, view: ViewSpecification): ViewSpecification =>{
242
243   // resolve all references.
244   view = { ...view };
245   view.elements = Object.keys(view.elements).reduce<{ [name: string]: ViewElement }>((acc, cur) => {
246     const resolveHistory : ViewElement[] = [];  
247     let elm = view.elements[cur];
248     const key = defaultNS && cur.replace(new RegExp(`^${defaultNS}:`, 'i'), '') || cur;
249     while (isViewElementReference(elm)) {
250       const result = (elm.ref(vPath));  
251       if (result) {
252         const [referencedElement, referencedPath] = result;
253         if (resolveHistory.some(hist => hist === referencedElement)) {
254           console.error(`Circle reference found at: ${vPath}`, resolveHistory);
255           break;
256         }
257         elm = referencedElement;
258         vPath = referencedPath;
259         resolveHistory.push(elm);
260       }
261     } 
262     
263     acc[key] = { ...elm, id: key };
264     
265     return acc;
266   }, {});
267   return view;
268 };
269
270 export const flattenViewElements = (defaultNS: string | null, parentPath: string, elements: { [name: string]: ViewElement }, views: ViewSpecification[], currentPath: string ): { [name: string]: ViewElement } => {
271   if (!elements) return {};
272   return Object.keys(elements).reduce<{ [name: string]: ViewElement }>((acc, cur) => {
273     const elm = elements[cur];
274
275     // remove the default namespace, and only the default namespace, sine it seems that this is also not in the restconf response
276     const elmKey = defaultNS && elm.id.replace(new RegExp(`^${defaultNS}:`, 'i'), '') || elm.id;
277     const key = parentPath ? `${parentPath}.${elmKey}` : elmKey;
278
279     if (isViewElementRpc(elm)) {
280       console.warn(`Flatten of RFC not supported ! [${currentPath}][${elm.label}]`);
281       return acc;
282     } else if (isViewElementObjectOrList(elm)) {
283       const view = views[+elm.viewId];
284       const inner = view && flattenViewElements(defaultNS, key, view.elements, views, `${currentPath}/${view.name}`);
285       if (inner) {
286         Object.keys(inner).forEach(k => (acc[k] = inner[k]));
287       }
288     } else if (isViewElementChoice(elm)) {
289       acc[key] = {
290         ...elm,
291         id: key,
292         cases: Object.keys(elm.cases).reduce<{ [name: string]: ViewElementChoiceCase }>((accCases, curCases) => {
293           const caseElement = elm.cases[curCases];
294           accCases[curCases] = {
295             ...caseElement,
296             // Hint: do not use key it contains elmKey, which shell be omitted for cases.
297             elements: flattenViewElements(defaultNS, /*key*/ parentPath, caseElement.elements, views, `${currentPath}/${elm.label}`),
298           };
299           return accCases;
300         }, {}),
301       };
302     } else {
303       acc[key] = {
304         ...elm,
305         id: key,
306       };
307     }
308     return acc;
309   }, {});
310 };
311
312 export const filterViewElements = async (vPath: string, viewData: any, viewSpecification: ViewSpecification) => {
313   // filter elements of viewSpecification by evaluating when property
314   return Object.keys(viewSpecification.elements).reduce(async (accPromise, cur) => {
315     const acc = await accPromise;
316     const elm = viewSpecification.elements[cur];
317     if (!elm.when || await evaluateWhen(vPath || '', elm.when, viewData).catch((ex) => {
318       console.warn(`Error evaluating when clause at: ${viewSpecification.name} for element: ${cur}`, ex);
319       return true;
320     })) {
321       acc.elements[cur] = elm;
322     }
323     return acc;
324   }, Promise.resolve({ ...viewSpecification, elements: {} as { [key: string]: ViewElement } }));
325 };
326
327 export const createViewData = (namespace: string | null, viewSpecification: ViewSpecification, views: ViewSpecification[]) => Object.keys(viewSpecification.elements).reduce<{ [name: string]: any }>((acc, cur) => {
328   const elm = viewSpecification.elements[cur];
329   let currentNamespace = namespace;
330   const key = elm.id;
331   if (elm.default) {
332     acc[key] = elm.default || '';
333   } else if (elm.uiType === 'boolean') {
334     acc[key] = false;
335   } else if (elm.uiType === 'number') {
336     acc[key] = 0;
337   } else if (elm.uiType === 'string') {
338     acc[key] = '';
339   } else if (isViewElementObject(elm)) {
340     const view = views[+elm.viewId];
341     if (view) {
342       if (view.ns) {
343         currentNamespace = view.ns;
344       }
345       acc[key] = createViewData(currentNamespace, view, views);
346     }
347   } else if (isViewElementList(elm)) {
348     acc[key] = [];
349   }
350   return acc;
351 }, {});