import { isEmpty, isNotUndefined, isUndefined } from '../../shared/empty.util'; import { MetadataMapInterface, MetadataValue, MetadataValueFilter, MetadatumViewModel } from './metadata.models'; import { groupBy, sortBy } from 'lodash'; /** * Utility class for working with DSpace object metadata. * * When specifying metadata keys, wildcards are supported, so `'*'` will match all keys, `'dc.date.*'` will * match all qualified dc dates, and so on. Exact keys will be evaluated (and matches returned) in the order * they are given. * * When multiple keys in a map match a given wildcard, they are evaluated in the order they are stored in * the map (alphanumeric if obtained from the REST api). If duplicate or overlapping keys are specified, the * first one takes precedence. For example, specifying `['dc.date', 'dc.*', '*']` will cause any `dc.date` * values to be evaluated (and returned, if matched) first, followed by any other `dc` metadata values, * followed by any other (non-dc) metadata values. */ export class Metadata { /** * Gets all matching metadata in the map(s). * * @param {MetadataMapInterface|MetadataMapInterface[]} mapOrMaps The source map(s). When multiple maps are given, they will be * checked in order, and only values from the first with at least one match will be returned. * @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above. * @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done. * @returns {MetadataValue[]} the matching values or an empty array. */ public static all(mapOrMaps: MetadataMapInterface | MetadataMapInterface[], keyOrKeys: string | string[], filter?: MetadataValueFilter): MetadataValue[] { const mdMaps: MetadataMapInterface[] = mapOrMaps instanceof Array ? mapOrMaps : [mapOrMaps]; const matches: MetadataValue[] = []; for (const mdMap of mdMaps) { for (const mdKey of Metadata.resolveKeys(mdMap, keyOrKeys)) { const candidates = mdMap[mdKey]; if (candidates) { for (const candidate of candidates) { if (Metadata.valueMatches(candidate as MetadataValue, filter)) { matches.push(candidate as MetadataValue); } } } } if (!isEmpty(matches)) { return matches; } } return matches; } /** * Like [[Metadata.all]], but only returns string values. * * @param {MetadataMapInterface|MetadataMapInterface[]} mapOrMaps The source map(s). When multiple maps are given, they will be * checked in order, and only values from the first with at least one match will be returned. * @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above. * @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done. * @returns {string[]} the matching string values or an empty array. */ public static allValues(mapOrMaps: MetadataMapInterface | MetadataMapInterface[], keyOrKeys: string | string[], filter?: MetadataValueFilter): string[] { return Metadata.all(mapOrMaps, keyOrKeys, filter).map((mdValue) => mdValue.value); } /** * Gets the first matching MetadataValue object in the map(s), or `undefined`. * * @param {MetadataMapInterface|MetadataMapInterface[]} mapOrMaps The source map(s). * @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above. * @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done. * @returns {MetadataValue} the first matching value, or `undefined`. */ public static first(mdMapOrMaps: MetadataMapInterface | MetadataMapInterface[], keyOrKeys: string | string[], filter?: MetadataValueFilter): MetadataValue { const mdMaps: MetadataMapInterface[] = mdMapOrMaps instanceof Array ? mdMapOrMaps : [mdMapOrMaps]; for (const mdMap of mdMaps) { for (const key of Metadata.resolveKeys(mdMap, keyOrKeys)) { const values: MetadataValue[] = mdMap[key] as MetadataValue[]; if (values) { return values.find((value: MetadataValue) => Metadata.valueMatches(value, filter)); } } } } /** * Like [[Metadata.first]], but only returns a string value, or `undefined`. * * @param {MetadataMapInterface|MetadataMapInterface[]} mapOrMaps The source map(s). * @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above. * @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done. * @returns {string} the first matching string value, or `undefined`. */ public static firstValue(mdMapOrMaps: MetadataMapInterface | MetadataMapInterface[], keyOrKeys: string | string[], filter?: MetadataValueFilter): string { const value = Metadata.first(mdMapOrMaps, keyOrKeys, filter); return isUndefined(value) ? undefined : value.value; } /** * Checks for a matching metadata value in the given map(s). * * @param {MetadataMapInterface|MetadataMapInterface[]} mapOrMaps The source map(s). * @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above. * @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done. * @returns {boolean} whether a match is found. */ public static has(mdMapOrMaps: MetadataMapInterface | MetadataMapInterface[], keyOrKeys: string | string[], filter?: MetadataValueFilter): boolean { return isNotUndefined(Metadata.first(mdMapOrMaps, keyOrKeys, filter)); } /** * Checks if a value matches a filter. * * @param {MetadataValue} mdValue the value to check. * @param {MetadataValueFilter} filter the filter to use. * @returns {boolean} whether the filter matches, or true if no filter is given. */ public static valueMatches(mdValue: MetadataValue, filter: MetadataValueFilter) { if (!filter) { return true; } else if (filter.language && filter.language !== mdValue.language) { return false; } else if (filter.value) { let fValue = filter.value; let mValue = mdValue.value; if (filter.ignoreCase) { fValue = filter.value.toLowerCase(); mValue = mdValue.value.toLowerCase(); } if (filter.substring) { return mValue.includes(fValue); } else { return mValue === fValue; } } return true; } /** * Gets the list of keys in the map limited by, and in the order given by `keyOrKeys`. * * @param {MetadataMapInterface} mdMap The source map. * @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above. */ private static resolveKeys(mdMap: MetadataMapInterface = {}, keyOrKeys: string | string[]): string[] { const inputKeys: string[] = keyOrKeys instanceof Array ? keyOrKeys : [keyOrKeys]; const outputKeys: string[] = []; for (const inputKey of inputKeys) { if (inputKey.includes('*')) { const inputKeyRegex = new RegExp('^' + inputKey.replace('.', '\.').replace('*', '.*') + '$'); for (const mapKey of Object.keys(mdMap)) { if (!outputKeys.includes(mapKey) && inputKeyRegex.test(mapKey)) { outputKeys.push(mapKey); } } } else if (mdMap.hasOwnProperty(inputKey) && !outputKeys.includes(inputKey)) { outputKeys.push(inputKey); } } return outputKeys; } /** * Creates an array of MetadatumViewModels from an existing MetadataMapInterface. * * @param {MetadataMapInterface} mdMap The source map. * @returns {MetadatumViewModel[]} List of metadata view models based on the source map. */ public static toViewModelList(mdMap: MetadataMapInterface): MetadatumViewModel[] { let metadatumList: MetadatumViewModel[] = []; Object.keys(mdMap) .sort() .forEach((key: string) => { const fields = mdMap[key].map( (metadataValue: MetadataValue, index: number) => Object.assign( {}, metadataValue, { order: index, key })); metadatumList = [...metadatumList, ...fields]; }); return metadatumList; } /** * Creates an MetadataMapInterface from an existing array of MetadatumViewModels. * * @param {MetadatumViewModel[]} viewModelList The source list. * @returns {MetadataMapInterface} Map with metadata values based on the source list. */ public static toMetadataMap(viewModelList: MetadatumViewModel[]): MetadataMapInterface { const metadataMap: MetadataMapInterface = {}; const groupedList = groupBy(viewModelList, (viewModel) => viewModel.key); Object.keys(groupedList) .sort() .forEach((key: string) => { const orderedValues = sortBy(groupedList[key], ['order']); metadataMap[key] = orderedValues.map((value: MetadataValue) => { const val = Object.assign({}, value); delete (val as any).order; delete (val as any).key; return val; } ) }); return metadataMap; } }