1/*
2 * Copyright (C) 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import {Timestamp} from 'common/time';
18import {TimeDuration} from 'common/time_duration';
19import {
20  PropertySource,
21  PropertyTreeNode,
22} from 'trace/tree_node/property_tree_node';
23
24export class PropertyTreeNodeFactory {
25  constructor(
26    private denylistProperties: string[] = [],
27    private visitPrototype = true,
28  ) {}
29
30  makePropertyRoot(
31    rootId: string,
32    rootName: string,
33    source: PropertySource,
34    value: any,
35  ): PropertyTreeNode {
36    return new PropertyTreeNode(rootId, rootName, source, value);
37  }
38
39  makeProtoProperty(
40    rootId: string,
41    name: string,
42    value: any,
43  ): PropertyTreeNode {
44    return this.makeProperty(rootId, name, PropertySource.PROTO, value);
45  }
46
47  makeDefaultProperty(
48    rootId: string,
49    name: string,
50    defaultValue: any,
51  ): PropertyTreeNode {
52    return this.makeSimpleChildProperty(
53      rootId,
54      name,
55      defaultValue,
56      PropertySource.DEFAULT,
57    );
58  }
59
60  makeCalculatedProperty(
61    rootId: string,
62    propertyName: string,
63    value: any,
64  ): PropertyTreeNode {
65    return this.makeProperty(
66      rootId,
67      propertyName,
68      PropertySource.CALCULATED,
69      value,
70    );
71  }
72
73  private makeProperty(
74    rootId: string,
75    name: string,
76    source: PropertySource,
77    value: any,
78  ): PropertyTreeNode {
79    if (this.hasInnerProperties(value)) {
80      return this.makeNestedProperty(rootId, name, source, value);
81    } else {
82      return this.makeSimpleChildProperty(rootId, name, value, source);
83    }
84  }
85
86  private makeNestedProperty(
87    rootId: string,
88    name: string,
89    source: PropertySource,
90    value: object | any[],
91  ): PropertyTreeNode {
92    const rootName = rootId.split(' ');
93
94    const innerRoot = this.makePropertyRoot(
95      name.length > 0 ? `${rootId}.${name}` : rootId,
96      name.length > 0 ? name : rootName.slice(1, rootName.length).join(' '),
97      source,
98      undefined,
99    );
100    this.addInnerProperties(innerRoot, value, source);
101
102    return innerRoot;
103  }
104
105  private makeSimpleChildProperty(
106    rootId: string,
107    key: string,
108    value: any,
109    source: PropertySource,
110  ): PropertyTreeNode {
111    return new PropertyTreeNode(`${rootId}.${key}`, key, source, value);
112  }
113
114  private hasInnerProperties(value: any): boolean {
115    if (!value) return false;
116    if (Array.isArray(value)) return value.length > 0;
117    if (this.isLongType(value)) return false;
118    if (value instanceof Timestamp) return false;
119    if (value instanceof TimeDuration) return false;
120    return typeof value === 'object' && Object.keys(value).length > 0;
121  }
122
123  private isLongType(value: any): boolean {
124    const typeOfVal = value.$type?.name ?? value.constructor?.name;
125    if (typeOfVal === 'Long' || typeOfVal === 'BigInt') return true;
126    return false;
127  }
128
129  private addInnerProperties(
130    root: PropertyTreeNode,
131    value: any,
132    source: PropertySource,
133  ): void {
134    if (Array.isArray(value)) {
135      this.addArrayProperties(root, value, source);
136    } else {
137      this.addObjectProperties(root, value, source);
138    }
139  }
140
141  private addArrayProperties(
142    root: PropertyTreeNode,
143    value: any,
144    source: PropertySource,
145  ) {
146    for (const [key, val] of Object.entries(value)) {
147      root.addOrReplaceChild(this.makeProperty(`${root.id}`, key, source, val));
148    }
149  }
150
151  private addObjectProperties(
152    root: PropertyTreeNode,
153    value: any,
154    source: PropertySource,
155  ) {
156    const keys = this.getValidPropertyNames(value);
157
158    for (const key of keys) {
159      root.addOrReplaceChild(
160        this.makeProperty(`${root.id}`, key, source, value[key]),
161      );
162    }
163  }
164
165  private getValidPropertyNames(objProto: any): string[] {
166    if (objProto === null || objProto === undefined) {
167      return [];
168    }
169    const props: string[] = [];
170    let obj = objProto;
171
172    do {
173      const properties = Object.getOwnPropertyNames(obj).filter((it) => {
174        if (typeof objProto[it] === 'function') return false;
175        if (it.includes(`$`)) return false;
176        if (it.startsWith(`_`)) return false;
177        if (this.denylistProperties.includes(it)) return false;
178
179        const value = objProto[it];
180        if (Array.isArray(value) && value.length > 0) return !value[0].stableId;
181
182        return value !== undefined;
183      });
184
185      properties.forEach((prop) => {
186        if (
187          typeof objProto[prop] !== 'function' &&
188          props.indexOf(prop) === -1
189        ) {
190          props.push(prop);
191        }
192      });
193      obj = this.visitPrototype ? Object.getPrototypeOf(obj) : undefined;
194    } while (obj);
195    return props;
196  }
197}
198
199export const DEFAULT_PROPERTY_TREE_NODE_FACTORY = new PropertyTreeNodeFactory();
200