@ui5/task-adaptation
Version:
Custom task for ui5-builder which allows building UI5 Flexibility Adaptation Projects for SAP BTP, Cloud Foundry environment
283 lines • 11.5 kB
JavaScript
import { insertInArray, traverse } from "../../util/commonUtil.js";
import AnnotationDiffStructureError from "../../model/annotationDiffStructureError.js";
import InterchangableCase from "./interchangableCase.js";
import MetadataJsonUtil from "../converter/metadataJsonUtil.js";
import XmlUtil from "../../util/xmlUtil.js";
export class Diff {
__old;
__new;
constructor(__old, __new) {
this.__old = __old;
this.__new = __new;
}
toString() {
return `{ __old: ${this.__old}, __new: ${this.__new} }`;
}
}
export default class Comparator {
diffs = new Set();
xml_a;
xml_b;
constructor(xml_a, xml_b) {
this.xml_a = xml_a;
this.xml_b = xml_b;
}
compare() {
const json_a = typeof this.xml_a === "string" ? XmlUtil.xmlToJson(this.xml_a) : this.xml_a;
const json_b = typeof this.xml_b === "string" ? XmlUtil.xmlToJson(this.xml_b) : this.xml_b;
const scheme_a = MetadataJsonUtil.getSchemaNode(json_a);
const scheme_b = MetadataJsonUtil.getSchemaNode(json_b);
if (scheme_a && scheme_b) {
// we compare only Annotations, other types are left as it is
this.traverseCompare(scheme_a, scheme_b, "Annotations");
}
return {
json: json_a,
properties: this.diffs
};
}
traverseCompare(obj_a, obj_b, property) {
let a = obj_a[property];
let b = obj_b[property];
if (typeof a === "object" && !(a instanceof Diff) && typeof b !== "object" ||
typeof b === "object" && !(b instanceof Diff) && typeof a !== "object" ||
a == null || b == null) {
// When during traversing we end up in primitives like string, we
// compare the values. If one of them is not primitive or oone of
// them is undefined, we throw exception (see test 06, 07).
throw new AnnotationDiffStructureError({ a, b });
}
else if (typeof a !== "object" && typeof b !== "object") {
// If primitive values are not same - we assume they are
// translations, so we save them.
if (a !== b) {
obj_a[property] = new Diff(a, b);
this.diffs.add({ object: obj_a, property });
}
}
else {
a = this.arrayIfNeeded(obj_a, obj_b, property);
b = this.arrayIfNeeded(obj_b, obj_a, property);
if (Array.isArray(a)) {
let idProperty = this.getIdProperty(property);
if (idProperty) {
this.traverseById(a, b, idProperty, property);
}
else {
for (let i = 0; i < Math.max(a.length, b.length); i++) {
if (a[i] && b[i]) {
this.traverseCompare(a, b, i);
}
else {
// If the number of items of nodes without id are
// different, we throw error (see test 08).
throw new AnnotationDiffStructureError({ a, b });
}
}
}
}
else {
for (const key of Object.keys(a)) {
this.traverseCompare(a, b, key);
}
}
}
}
/**
* If one language annotation has one property it is an object, if other
* language same annotation consists of multiple properties, we need to
* equal them, so they are both arrays (see test 01-04).
*/
arrayIfNeeded(obj_a, obj_b, property) {
if (!Array.isArray(obj_a[property])) {
// If node with id - make array anyway, so it's easier to compare (see test 04).
if (simpleIdentifiers.has(property) || Array.isArray(obj_b[property])) {
obj_a[property] = [obj_a[property]];
}
}
return obj_a[property];
}
/**
* If some node (Annotations, Annotation, PropertyValue, LabeledElement) has
* an id, we can compare by id, so the items order doesn't matter anymore.
* @param a array of nodes with id of one language
* @param b array of nodes with id of the other language
* @param idProperty property which value is an id (e.g. Target="<unique-id>")
* @param property node name (Annotations, Annotation, PropertyValue, ...)
*/
traverseById(a, b, idProperty, property) {
if (typeof property !== "string") {
return;
}
let items_a = new Items(a, idProperty);
let items_b = new Items(b, idProperty);
const includer_a = new Includer(a, property);
const includer_b = new Includer(b, property);
for (let i = 0; i < Math.max(a.length, b.length); i++) {
// There might be an exceptional case, when the object doesn't
// contain attributes. In this case we continue traversing, like
// UI5 does.
const id_a = a[i]?._attributes?.[idProperty];
const id_b = b[i]?._attributes?.[idProperty];
if (id_a !== id_b) {
// We go down the array and if suddenly the ids for comparing
// items are not the same, we need to find the item with the
// same id if it exists.
if (items_b.has(id_a) && items_a.has(id_b)) {
// If we found the item with the same id, we swap places
// with current item (see test 05).
items_b.swap(id_a, i);
}
else if (!items_a.has(id_b) && id_b) {
// If 1st language missing the item, include it from 2nd (see test 02).
includer_a.include(b, i);
}
else if (!items_b.has(id_a) && id_a) {
// If 2nd language missing the item, include it from 1st (see test 03).
includer_b.include(a, i);
}
}
this.traverseCompare(a, b, i);
}
}
/**
* Some nodes, like Annotations, Annotation, PropertyValue have unique id
* among other same nodes. We can use it to know what to compare with what
* even if the order is different. IdProperty is a property name of that id,
* e.g. for Annotations it will be Target (like in
* Target="<some-unique-id>").
* @param property node which might have an id: Annotations, PropertyValue
* @return the property name which represents id: Target, Property
*/
getIdProperty(property) {
return simpleIdentifiers.get(property);
}
}
class Includer {
ALL_DIFF_CASES = [
new InterchangableCase()
];
diffCases = new Array();
shouldClear;
target;
property;
/**
* It will decide what to do with the item missing in one language.
* @param target an array which might miss the item
* @param property the node name needed to decide how to include the missing
* item in array
* @param shouldClear if the item is missing and it's not Label or QuickInfo
* or Heading, we clear all the properties except Ids, so in i18n.properties
* they will have empty values. Because we don't know what should be there.
*/
constructor(target, property, shouldClear = true) {
this.shouldClear = shouldClear;
this.target = target;
this.property = property;
for (const diffCase of this.ALL_DIFF_CASES) {
if (diffCase.canAccept(target, property)) {
this.diffCases.push(diffCase);
}
}
}
/**
* If in some language some array missing the item, we include the missing
* item from other language array with the same id.
* @param source the item from other language array that is missed in target
* @param index here to put it in the array
*/
include(source, index) {
// Insert node with empty value (see test 02) if missing in default
// language or default language value if missing in other language
// (see test 03).
const clone = Includer.cloneAndClear(source[index], this.shouldClear);
insertInArray(this.target, index, clone);
// Some annotations like Label, QuickInfo or Heading are
// interchangable so if QuickInfo is missing we can copy the value
// from Label or Heading (see test 01).
for (const diffCase of this.diffCases) {
diffCase.accept(this.target, index, this.property);
}
}
/**
* if the item is missing in default language, and it's not Label or
* QuickInfo or Heading, we clear all the properties except Ids, so in
* i18n.properties they will have empty values. Because we don't know what
* should be there. But if the item is missing in language other than
* default, we include the copy of item from default language and not
* clearing them (see test 02, 04).
*/
static cloneAndClear(obj, shouldClear = true) {
const clone = structuredClone(obj);
if (shouldClear) {
traverse(clone, [], (json, key) => {
if (typeof key !== "string" || !simpleIdentifiersReversed.has(key)) {
json[key] = "";
}
});
}
return clone;
}
}
class Items {
idProperty;
array;
objectMap = null;
/**
* Map of id per item which is lazy initialized if needed
* @param array
* @param idProperty
*/
constructor(array, idProperty) {
this.array = array;
this.idProperty = idProperty;
}
/**
* Find the item with by id and swap their places.
* @param id of the item which seems like not in the place it should be
* @param newIndex new place where the item should actually be
*/
swap(id, newIndex) {
const oldIndex = this.initMap().get(id);
const temp = this.array[newIndex];
this.array[newIndex] = this.array[oldIndex];
this.array[oldIndex] = temp;
this.initMap(true);
}
has(idProperty) {
return this.initMap().has(idProperty);
}
get(idProperty) {
return this.array[this.initMap().get(idProperty)];
}
/**
* Lazy init the map only if the order of items are messed up, which
* actually an eexception, so will make it lazy way.
* @param force force to update.
* @returns the map id per item index.
*/
initMap(force = false) {
if (this.objectMap == null || force) {
this.objectMap = new Map(this.array.map((item, index) => [item._attributes[this.idProperty], index]));
}
return this.objectMap;
}
}
class Identifiers extends Map {
has(property) {
return typeof property === "string" && super.has(property);
}
get(property) {
return typeof property === "string" ? super.get(property) : undefined;
}
}
// According to OData schema some nodes MUST have the ids, by these nodes the
// property which contains the id is named differently as you can see.
const simpleIdentifiers = new Identifiers([
["Annotations", "Target"],
["Annotation", "Term"],
["LabeledElement", "Name"],
["PropertyValue", "Property"]
]);
const simpleIdentifiersReversed = new Identifiers([...simpleIdentifiers].map(([name, idProperty]) => [idProperty, name]));
//# sourceMappingURL=comparator.js.map