diff --git a/src/app/+item-page/edit-item-page/modify-item-overview/modify-item-overview.component.html b/src/app/+item-page/edit-item-page/modify-item-overview/modify-item-overview.component.html index d59d29ddbf..861c20ada7 100644 --- a/src/app/+item-page/edit-item-page/modify-item-overview/modify-item-overview.component.html +++ b/src/app/+item-page/edit-item-page/modify-item-overview/modify-item-overview.component.html @@ -7,10 +7,12 @@ - - {{metadatum.key}} - {{metadatum.value}} - {{metadatum.language}} - + + + {{entry.key}} + {{value.value}} + {{value.language}} + + - \ No newline at end of file + diff --git a/src/app/+item-page/edit-item-page/modify-item-overview/modify-item-overview.component.ts b/src/app/+item-page/edit-item-page/modify-item-overview/modify-item-overview.component.ts index d32a98d5e0..282f8687e1 100644 --- a/src/app/+item-page/edit-item-page/modify-item-overview/modify-item-overview.component.ts +++ b/src/app/+item-page/edit-item-page/modify-item-overview/modify-item-overview.component.ts @@ -1,6 +1,6 @@ import {Component, Input, OnInit} from '@angular/core'; import {Item} from '../../../core/shared/item.model'; -import {Metadatum} from '../../../core/shared/metadatum.model'; +import {MetadataMap} from '../../../core/shared/metadata.interfaces'; @Component({ selector: 'ds-modify-item-overview', @@ -12,7 +12,7 @@ import {Metadatum} from '../../../core/shared/metadatum.model'; export class ModifyItemOverviewComponent implements OnInit { @Input() item: Item; - metadata: Metadatum[]; + metadata: MetadataMap; ngOnInit(): void { this.metadata = this.item.metadata; diff --git a/src/app/+item-page/field-components/metadata-uri-values/metadata-uri-values.component.html b/src/app/+item-page/field-components/metadata-uri-values/metadata-uri-values.component.html index cc618bcd50..344a32841b 100644 --- a/src/app/+item-page/field-components/metadata-uri-values/metadata-uri-values.component.html +++ b/src/app/+item-page/field-components/metadata-uri-values/metadata-uri-values.component.html @@ -1,5 +1,5 @@ - - {{ linktext || metadatum.value }} + + {{ linktext || value.value }} diff --git a/src/app/+item-page/field-components/metadata-uri-values/metadata-uri-values.component.ts b/src/app/+item-page/field-components/metadata-uri-values/metadata-uri-values.component.ts index 212dcddee8..b5870f16b8 100644 --- a/src/app/+item-page/field-components/metadata-uri-values/metadata-uri-values.component.ts +++ b/src/app/+item-page/field-components/metadata-uri-values/metadata-uri-values.component.ts @@ -1,6 +1,7 @@ import { Component, Input } from '@angular/core'; import { MetadataValuesComponent } from '../metadata-values/metadata-values.component'; +import { MetadataValue } from '../../../core/shared/metadata.interfaces'; /** * This component renders the configured 'values' into the ds-metadata-field-wrapper component as a link. @@ -18,7 +19,7 @@ export class MetadataUriValuesComponent extends MetadataValuesComponent { @Input() linktext: any; - @Input() values: any; + @Input() values: MetadataValue[]; @Input() separator: string; diff --git a/src/app/+item-page/field-components/metadata-values/metadata-values.component.html b/src/app/+item-page/field-components/metadata-values/metadata-values.component.html index f16655c63c..fbc1e11a07 100644 --- a/src/app/+item-page/field-components/metadata-values/metadata-values.component.html +++ b/src/app/+item-page/field-components/metadata-values/metadata-values.component.html @@ -1,5 +1,5 @@ - - {{metadatum.value}} + + {{value.value}} diff --git a/src/app/+item-page/field-components/metadata-values/metadata-values.component.ts b/src/app/+item-page/field-components/metadata-values/metadata-values.component.ts index 1c94b56d57..2be7291dcd 100644 --- a/src/app/+item-page/field-components/metadata-values/metadata-values.component.ts +++ b/src/app/+item-page/field-components/metadata-values/metadata-values.component.ts @@ -1,5 +1,5 @@ import { Component, Input } from '@angular/core'; -import { Metadatum } from '../../../core/shared/metadatum.model'; +import { MetadataValue } from '../../../core/shared/metadata.interfaces'; /** * This component renders the configured 'values' into the ds-metadata-field-wrapper component. @@ -12,7 +12,7 @@ import { Metadatum } from '../../../core/shared/metadatum.model'; }) export class MetadataValuesComponent { - @Input() values: Metadatum[]; + @Input() values: MetadataValue[]; @Input() separator: string; diff --git a/src/app/+item-page/full/field-components/file-section/full-file-section.component.html b/src/app/+item-page/full/field-components/file-section/full-file-section.component.html index d5a7febeb9..a68993cd16 100644 --- a/src/app/+item-page/full/field-components/file-section/full-file-section.component.html +++ b/src/app/+item-page/full/field-components/file-section/full-file-section.component.html @@ -17,7 +17,7 @@
{{"item.page.filesection.description" | translate}}
-
{{file.findMetadata("dc.description")}}
+
{{file.firstMetadataValue("dc.description")}}
diff --git a/src/app/+item-page/full/full-item-page.component.html b/src/app/+item-page/full/full-item-page.component.html index 1d0c4ab812..7080ba2e96 100644 --- a/src/app/+item-page/full/full-item-page.component.html +++ b/src/app/+item-page/full/full-item-page.component.html @@ -9,11 +9,13 @@
- - - - - + + + + + + +
{{metadatum.key}}{{metadatum.value}}{{metadatum.language}}
{{entry.key}}{{value.value}}{{value.language}}
diff --git a/src/app/+item-page/full/full-item-page.component.ts b/src/app/+item-page/full/full-item-page.component.ts index d09ac268ec..fcb724b564 100644 --- a/src/app/+item-page/full/full-item-page.component.ts +++ b/src/app/+item-page/full/full-item-page.component.ts @@ -6,7 +6,7 @@ import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; import { ItemPageComponent } from '../simple/item-page.component'; -import { Metadatum } from '../../core/shared/metadatum.model'; +import { MetadataMap } from '../../core/shared/metadata.interfaces'; import { ItemDataService } from '../../core/data/item-data.service'; import { RemoteData } from '../../core/data/remote-data'; @@ -34,7 +34,7 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit { itemRD$: Observable>; - metadata$: Observable; + metadata$: Observable; constructor(route: ActivatedRoute, items: ItemDataService, metadataService: MetadataService) { super(route, items, metadataService); diff --git a/src/app/+item-page/simple/field-components/specific-field/item-page-specific-field.component.html b/src/app/+item-page/simple/field-components/specific-field/item-page-specific-field.component.html index 4a27848ec6..c0d948fc76 100644 --- a/src/app/+item-page/simple/field-components/specific-field/item-page-specific-field.component.html +++ b/src/app/+item-page/simple/field-components/specific-field/item-page-specific-field.component.html @@ -1,3 +1,3 @@
- +
diff --git a/src/app/+item-page/simple/field-components/specific-field/title/item-page-title-field.component.html b/src/app/+item-page/simple/field-components/specific-field/title/item-page-title-field.component.html index 4c53e2e3e2..40f54ec636 100644 --- a/src/app/+item-page/simple/field-components/specific-field/title/item-page-title-field.component.html +++ b/src/app/+item-page/simple/field-components/specific-field/title/item-page-title-field.component.html @@ -1,3 +1,3 @@

- +

diff --git a/src/app/+item-page/simple/field-components/specific-field/uri/item-page-uri-field.component.html b/src/app/+item-page/simple/field-components/specific-field/uri/item-page-uri-field.component.html index fde79d6a04..771fef53fc 100644 --- a/src/app/+item-page/simple/field-components/specific-field/uri/item-page-uri-field.component.html +++ b/src/app/+item-page/simple/field-components/specific-field/uri/item-page-uri-field.component.html @@ -1,3 +1,3 @@
- +
diff --git a/src/app/+search-page/normalized-search-result.model.ts b/src/app/+search-page/normalized-search-result.model.ts index 0683c74aed..3c1a46872c 100644 --- a/src/app/+search-page/normalized-search-result.model.ts +++ b/src/app/+search-page/normalized-search-result.model.ts @@ -1,5 +1,5 @@ import { autoserialize } from 'cerialize'; -import { Metadatum } from '../core/shared/metadatum.model'; +import { MetadataMap } from '../core/shared/metadata.interfaces'; import { ListableObject } from '../shared/object-collection/shared/listable-object.model'; /** @@ -16,6 +16,6 @@ export class NormalizedSearchResult implements ListableObject { * The metadata that was used to find this item, hithighlighted */ @autoserialize - hitHighlights: Metadatum[]; + hitHighlights: MetadataMap; } diff --git a/src/app/+search-page/search-result.model.ts b/src/app/+search-page/search-result.model.ts index 00b1c62a99..b2e5eafdec 100644 --- a/src/app/+search-page/search-result.model.ts +++ b/src/app/+search-page/search-result.model.ts @@ -1,5 +1,5 @@ import { DSpaceObject } from '../core/shared/dspace-object.model'; -import { Metadatum } from '../core/shared/metadatum.model'; +import { MetadataMap } from '../core/shared/metadata.interfaces'; import { ListableObject } from '../shared/object-collection/shared/listable-object.model'; /** @@ -14,6 +14,6 @@ export class SearchResult implements ListableObject { /** * The metadata that was used to find this item, hithighlighted */ - hitHighlights: Metadatum[]; + hitHighlights: MetadataMap; } diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts index b807a77e99..41675632dd 100644 --- a/src/app/core/browse/browse.service.ts +++ b/src/app/core/browse/browse.service.ts @@ -38,8 +38,8 @@ import { DSpaceObject } from '../shared/dspace-object.model'; export class BrowseService { protected linkPath = 'browses'; - private static toSearchKeyArray(metadatumKey: string): string[] { - const keyParts = metadatumKey.split('.'); + private static toSearchKeyArray(metadataKey: string): string[] { + const keyParts = metadataKey.split('.'); const searchFor = []; searchFor.push('*'); for (let i = 0; i < keyParts.length - 1; i++) { @@ -47,7 +47,7 @@ export class BrowseService { const nextPart = [...prevParts, '*'].join('.'); searchFor.push(nextPart); } - searchFor.push(metadatumKey); + searchFor.push(metadataKey); return searchFor; } @@ -179,8 +179,8 @@ export class BrowseService { return this.rdb.toRemoteDataObservable(requestEntry$, payload$); } - getBrowseURLFor(metadatumKey: string, linkPath: string): Observable { - const searchKeyArray = BrowseService.toSearchKeyArray(metadatumKey); + getBrowseURLFor(metadataKey: string, linkPath: string): Observable { + const searchKeyArray = BrowseService.toSearchKeyArray(metadataKey); return this.getBrowseDefinitions().pipe( getRemoteDataPayload(), map((browseDefinitions: BrowseDefinition[]) => browseDefinitions @@ -191,7 +191,7 @@ export class BrowseService { ), map((def: BrowseDefinition) => { if (isEmpty(def) || isEmpty(def._links) || isEmpty(def._links[linkPath])) { - throw new Error(`A browse endpoint for ${linkPath} on ${metadatumKey} isn't configured`); + throw new Error(`A browse endpoint for ${linkPath} on ${metadataKey} isn't configured`); } else { return def._links[linkPath]; } diff --git a/src/app/core/cache/models/normalized-dspace-object.model.ts b/src/app/core/cache/models/normalized-dspace-object.model.ts index 030e17364a..87bd4b4369 100644 --- a/src/app/core/cache/models/normalized-dspace-object.model.ts +++ b/src/app/core/cache/models/normalized-dspace-object.model.ts @@ -1,7 +1,6 @@ import { autoserialize, autoserializeAs, deserialize, serialize } from 'cerialize'; import { DSpaceObject } from '../../shared/dspace-object.model'; - -import { Metadatum } from '../../shared/metadatum.model'; +import { MetadataMap } from '../../shared/metadata.interfaces'; import { ResourceType } from '../../shared/resource-type'; import { mapsTo } from '../builders/build-decorators'; import { NormalizedObject } from './normalized-object.model'; @@ -46,10 +45,10 @@ export class NormalizedDSpaceObject extends NormalizedObject { type: ResourceType; /** - * An array containing all metadata of this DSpaceObject + * All metadata of this DSpaceObject */ - @autoserializeAs(Metadatum) - metadata: Metadatum[]; + @autoserialize + metadata: MetadataMap; /** * An array of DSpaceObjects that are direct parents of this DSpaceObject diff --git a/src/app/core/data/search-response-parsing.service.ts b/src/app/core/data/search-response-parsing.service.ts index 7ee2b60f89..46b2572c4e 100644 --- a/src/app/core/data/search-response-parsing.service.ts +++ b/src/app/core/data/search-response-parsing.service.ts @@ -7,7 +7,7 @@ import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response. import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; import { hasValue } from '../../shared/empty.util'; import { SearchQueryResponse } from '../../+search-page/search-service/search-query-response.model'; -import { Metadatum } from '../shared/metadatum.model'; +import { MetadataMap, MetadataValue } from '../shared/metadata.interfaces'; @Injectable() export class SearchResponseParsingService implements ResponseParsingService { @@ -16,17 +16,17 @@ export class SearchResponseParsingService implements ResponseParsingService { parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { const payload = data.payload._embedded.searchResult; - const hitHighlights = payload._embedded.objects + const hitHighlights: MetadataMap[] = payload._embedded.objects .map((object) => object.hitHighlights) .map((hhObject) => { + const mdMap: MetadataMap = {}; if (hhObject) { - return Object.keys(hhObject).map((key) => Object.assign(new Metadatum(), { - key: key, - value: hhObject[key].join('...') - })) - } else { - return []; + for (const key of Object.keys(hhObject)) { + const value: MetadataValue = { value: hhObject[key].join('...'), language: null }; + mdMap[key] = [ value ]; + } } + return mdMap; }); const dsoSelfLinks = payload._embedded.objects diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts index 20d6b1dfb3..6bf5eb0818 100644 --- a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts +++ b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts @@ -91,9 +91,9 @@ export class DSpaceRESTv2Service { const form: FormData = new FormData(); form.append('name', dso.name); if (dso.metadata) { - for (const i of Object.keys(dso.metadata)) { - if (isNotEmpty(dso.metadata[i].value)) { - form.append(dso.metadata[i].key, dso.metadata[i].value); + for (const key of Object.keys(dso.metadata)) { + for (const value of dso.allMetadataValues(key)) { + form.append(key, value); } } } diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index 9dbfae3f90..736bf11923 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -294,6 +294,10 @@ export class MetadataService { } } + private hasType(value: string): boolean { + return this.currentObject.value.hasMetadata('dc.type', { value: value, ignoreCase: true }); + } + /** * Returns true if this._item is a dissertation * @@ -301,14 +305,7 @@ export class MetadataService { * true if this._item has a dc.type equal to 'Thesis' */ private isDissertation(): boolean { - let isDissertation = false; - for (const metadatum of this.currentObject.value.metadata) { - if (metadatum.key === 'dc.type') { - isDissertation = metadatum.value.toLowerCase() === 'thesis'; - break; - } - } - return isDissertation; + return this.hasType('thesis'); } /** @@ -318,40 +315,15 @@ export class MetadataService { * true if this._item has a dc.type equal to 'Technical Report' */ private isTechReport(): boolean { - let isTechReport = false; - for (const metadatum of this.currentObject.value.metadata) { - if (metadatum.key === 'dc.type') { - isTechReport = metadatum.value.toLowerCase() === 'technical report'; - break; - } - } - return isTechReport; + return this.hasType('technical report'); } private getMetaTagValue(key: string): string { - let value: string; - for (const metadatum of this.currentObject.value.metadata) { - if (metadatum.key === key) { - value = metadatum.value; - } - } - return value; + return this.currentObject.value.firstMetadataValue(key); } private getFirstMetaTagValue(keys: string[]): string { - let value: string; - for (const metadatum of this.currentObject.value.metadata) { - for (const key of keys) { - if (key === metadatum.key) { - value = metadatum.value; - break; - } - } - if (value !== undefined) { - break; - } - } - return value; + return this.currentObject.value.firstMetadataValue(keys); } private getMetaTagValuesAndCombine(key: string): string { @@ -359,15 +331,7 @@ export class MetadataService { } private getMetaTagValues(keys: string[]): string[] { - const values: string[] = []; - for (const metadatum of this.currentObject.value.metadata) { - for (const key of keys) { - if (key === metadatum.key) { - values.push(metadatum.value); - } - } - } - return values; + return this.currentObject.value.allMetadataValues(keys); } private addMetaTag(property: string, content: string): void { diff --git a/src/app/core/shared/collection.model.ts b/src/app/core/shared/collection.model.ts index 8fdc14bd6e..c630c9dd57 100644 --- a/src/app/core/shared/collection.model.ts +++ b/src/app/core/shared/collection.model.ts @@ -16,7 +16,7 @@ export class Collection extends DSpaceObject { * Corresponds to the metadata field dc.description */ get introductoryText(): string { - return this.findMetadata('dc.description'); + return this.firstMetadataValue('dc.description'); } /** @@ -24,7 +24,7 @@ export class Collection extends DSpaceObject { * Corresponds to the metadata field dc.description.abstract */ get shortDescription(): string { - return this.findMetadata('dc.description.abstract'); + return this.firstMetadataValue('dc.description.abstract'); } /** @@ -32,7 +32,7 @@ export class Collection extends DSpaceObject { * Corresponds to the metadata field dc.rights */ get copyrightText(): string { - return this.findMetadata('dc.rights'); + return this.firstMetadataValue('dc.rights'); } /** @@ -40,7 +40,7 @@ export class Collection extends DSpaceObject { * Corresponds to the metadata field dc.rights.license */ get license(): string { - return this.findMetadata('dc.rights.license'); + return this.firstMetadataValue('dc.rights.license'); } /** @@ -48,7 +48,7 @@ export class Collection extends DSpaceObject { * Corresponds to the metadata field dc.description.tableofcontents */ get sidebarText(): string { - return this.findMetadata('dc.description.tableofcontents'); + return this.firstMetadataValue('dc.description.tableofcontents'); } /** diff --git a/src/app/core/shared/community.model.ts b/src/app/core/shared/community.model.ts index c5f6e0ab87..c4e703fd7f 100644 --- a/src/app/core/shared/community.model.ts +++ b/src/app/core/shared/community.model.ts @@ -17,7 +17,7 @@ export class Community extends DSpaceObject { * Corresponds to the metadata field dc.description */ get introductoryText(): string { - return this.findMetadata('dc.description'); + return this.firstMetadataValue('dc.description'); } /** @@ -25,7 +25,7 @@ export class Community extends DSpaceObject { * Corresponds to the metadata field dc.description.abstract */ get shortDescription(): string { - return this.findMetadata('dc.description.abstract'); + return this.firstMetadataValue('dc.description.abstract'); } /** @@ -33,7 +33,7 @@ export class Community extends DSpaceObject { * Corresponds to the metadata field dc.rights */ get copyrightText(): string { - return this.findMetadata('dc.rights'); + return this.firstMetadataValue('dc.rights'); } /** @@ -41,7 +41,7 @@ export class Community extends DSpaceObject { * Corresponds to the metadata field dc.description.tableofcontents */ get sidebarText(): string { - return this.findMetadata('dc.description.tableofcontents'); + return this.firstMetadataValue('dc.description.tableofcontents'); } /** diff --git a/src/app/core/shared/dspace-object.model.ts b/src/app/core/shared/dspace-object.model.ts index 3e08da151c..1dba79865f 100644 --- a/src/app/core/shared/dspace-object.model.ts +++ b/src/app/core/shared/dspace-object.model.ts @@ -1,5 +1,5 @@ -import { Metadatum } from './metadatum.model' -import { isEmpty, isNotEmpty } from '../../shared/empty.util'; +import { MetadataMap, MetadataValue, MetadataValueFilter } from './metadata.interfaces'; +import { Metadata } from './metadata.model'; import { CacheableObject } from '../cache/object-cache.reducer'; import { RemoteData } from '../data/remote-data'; import { ResourceType } from './resource-type'; @@ -35,14 +35,14 @@ export class DSpaceObject implements CacheableObject, ListableObject { * The name for this DSpaceObject */ get name(): string { - return this.findMetadata('dc.title'); + return this.firstMetadataValue('dc.title'); } /** - * An array containing all metadata of this DSpaceObject + * All metadata of this DSpaceObject */ @autoserialize - metadata: Metadatum[] = []; + metadata: MetadataMap; /** * An array of DSpaceObjects that are direct parents of this DSpaceObject @@ -54,42 +54,29 @@ export class DSpaceObject implements CacheableObject, ListableObject { */ owner: Observable>; - /** - * Find a metadata field by key and language - * - * This method returns the value of the first element - * in the metadata array that matches the provided - * key and language - * - * @param key - * @param language - * @return string - */ - findMetadata(key: string, language?: string): string { - const metadatum = this.metadata.find((m: Metadatum) => { - return m.key === key && (isEmpty(language) || m.language === language) - }); - if (isNotEmpty(metadatum)) { - return metadatum.value; - } else { - return undefined; - } + /** Gets all matching metadata in this DSpaceObject. See `Metadata.all` for more information. */ + allMetadata(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): MetadataValue[] { + return Metadata.all(this.metadata, keyOrKeys, valueFilter); } - /** - * Find metadata by an array of keys - * - * This method returns the values of the element - * in the metadata array that match the provided - * key(s) - * - * @param key(s) - * @return Array - */ - filterMetadata(keys: string[]): Metadatum[] { - return this.metadata.filter((metadatum: Metadatum) => { - return keys.some((key) => key === metadatum.key); - }); + /** Like `allMetadata`, but only returns string values. */ + allMetadataValues(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string[] { + return Metadata.allValues(this.metadata, keyOrKeys, valueFilter); + } + + /** Gets the first matching metadata in this DSpaceObject, or `undefined`. */ + firstMetadata(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): MetadataValue { + return Metadata.first(this.metadata, keyOrKeys, valueFilter); + } + + /** Like `firstMetadata`, but only returns a string value, or `undefined`. */ + firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string { + return Metadata.firstValue(this.metadata, keyOrKeys, valueFilter); + } + + /** Checks for matching metadata in this DSpaceObject. */ + hasMetadata(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): boolean { + return Metadata.has(this.metadata, keyOrKeys, valueFilter); } } diff --git a/src/app/core/shared/metadata.interfaces.ts b/src/app/core/shared/metadata.interfaces.ts new file mode 100644 index 0000000000..3590117ce8 --- /dev/null +++ b/src/app/core/shared/metadata.interfaces.ts @@ -0,0 +1,30 @@ +/** A map of metadata keys to an ordered list of MetadataValue objects. */ +export interface MetadataMap { + [ key: string ]: MetadataValue[]; +} + +/** A single metadata value and its properties. */ +export interface MetadataValue { + + /** The language. */ + language: string; + + /** The string value. */ + value: string; +} + +/** Constraints for matching metadata values. */ +export interface MetadataValueFilter { + + /** The language constraint. */ + language?: string; + + /** The value constraint. */ + value?: string; + + /** Whether the value constraint should match without regard to case. */ + ignoreCase?: boolean; + + /** Whether the value constraint should match as a substring. */ + substring?: boolean; +} diff --git a/src/app/core/shared/metadata.model.ts b/src/app/core/shared/metadata.model.ts new file mode 100644 index 0000000000..c774af189b --- /dev/null +++ b/src/app/core/shared/metadata.model.ts @@ -0,0 +1,119 @@ +import { isEmpty } from '../../shared/empty.util'; +import { MetadataMap, MetadataValue, MetadataValueFilter } from './metadata.interfaces'; + +/** + * Static utility methods for working with DSpace metadata. + */ +export class Metadata { + + /** + * Gets all matching metadata. + * + * @param {MetadataMap|MetadataMap[]} mapOrMaps The source map(s). Values will only be returned from one map -- + * the first with at least one match. + * @param {string|string[]} keyOrKeys The metadata key(s) in scope. 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 in the parameter. When multiple keys match a 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. + * @param {MetadataValueFilter} filter The value filter to use. + * @returns {MetadataValue[]} the matching values or an empty array. + */ + public static all(mapOrMaps: MetadataMap | MetadataMap[], keyOrKeys: string | string[], + filter?: MetadataValueFilter): MetadataValue[] { + const mdMaps: MetadataMap[] = 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, filter)) { + matches.push(candidate); + } + } + } + } + if (!isEmpty(matches)) { + return matches; + } + } + return matches; + } + + /** Like `all`, but only returns string values. */ + public static allValues(mapOrMaps: MetadataMap | MetadataMap[], keyOrKeys: string | string[], + filter?: MetadataValueFilter): string[] { + return Metadata.all(mapOrMaps, keyOrKeys, filter).map((mdValue) => mdValue.value); + } + + /** Gets the first matching MetadataValue object in a map, or `undefined`. */ + public static first(mdMapOrMaps: MetadataMap | MetadataMap[], keyOrKeys: string | string[], + filter?: MetadataValueFilter): MetadataValue { + const mdMaps: MetadataMap[] = mdMapOrMaps instanceof Array ? mdMapOrMaps : [ mdMapOrMaps ]; + for (const mdMap of mdMaps) { + for (const key of Metadata.resolveKeys(mdMap, keyOrKeys)) { + const values: MetadataValue[] = mdMap[key]; + if (values) { + return values.find((value: MetadataValue) => Metadata.valueMatches(value, filter)); + } + } + } + } + + /** Like `first`, but only returns a string value, or `undefined`. */ + public static firstValue(mdMapOrMaps: MetadataMap | MetadataMap[], keyOrKeys: string | string[], + filter?: MetadataValueFilter): string { + const value = Metadata.first(mdMapOrMaps, keyOrKeys, filter); + return value === undefined ? undefined : value.value; + } + + /** Checks the given map for a matching value. */ + public static has(mdMapOrMaps: MetadataMap | MetadataMap[], keyOrKeys: string | string[], + filter?: MetadataValueFilter): boolean { + return Metadata.first(mdMapOrMaps, keyOrKeys, filter) !== undefined; + } + + /** Checks if a value matches a filter. */ + 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` */ + private static resolveKeys(mdMap: MetadataMap, 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; + } +} diff --git a/src/app/core/shared/metadatum.model.ts b/src/app/core/shared/metadatum.model.ts deleted file mode 100644 index a3c5830608..0000000000 --- a/src/app/core/shared/metadatum.model.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { autoserialize } from 'cerialize'; - -export class Metadatum { - - /** - * The metadata field of this Metadatum - */ - @autoserialize - key: string; - - /** - * The language of this Metadatum - */ - @autoserialize - language: string; - - /** - * The value of this Metadatum - */ - @autoserialize - value: string; - -} diff --git a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts index 17710fd1c6..0e0195aaaa 100644 --- a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts +++ b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts @@ -8,6 +8,7 @@ import { FormGroup } from '@angular/forms'; import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model'; import { TranslateService } from '@ngx-translate/core'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { MetadataMap, MetadataValue } from '../../../core/shared/metadata.interfaces'; import { isNotEmpty } from '../../empty.util'; import { ResourceType } from '../../../core/shared/resource-type'; @@ -64,7 +65,7 @@ export class ComColFormComponent implements OnInit { ngOnInit(): void { this.formModel.forEach( (fieldModel: DynamicInputModel) => { - fieldModel.value = this.dso.findMetadata(fieldModel.name); + fieldModel.value = this.dso.firstMetadataValue(fieldModel.name); } ); this.formGroup = this.formService.createFormGroup(this.formModel); @@ -77,20 +78,24 @@ export class ComColFormComponent implements OnInit { } /** - * Checks which new fields where added and sends the updated version of the DSO to the parent component + * Checks which new fields were added and sends the updated version of the DSO to the parent component */ onSubmit() { - const metadata = this.formModel.map( - (fieldModel: DynamicInputModel) => { - return { key: fieldModel.name, value: fieldModel.value } + const formMetadata = new Object() as MetadataMap; + this.formModel.forEach((fieldModel: DynamicInputModel) => { + const value: MetadataValue = { value: fieldModel.value as string, language: null }; + if (formMetadata.hasOwnProperty(fieldModel.name)) { + formMetadata[fieldModel.name].push(value); + } else { + formMetadata[fieldModel.name] = [ value ]; } - ); - const filteredOldMetadata = this.dso.metadata.filter((filter) => !metadata.map((md) => md.key).includes(filter.key)); - const filteredNewMetadata = metadata.filter((md) => isNotEmpty(md.value)); + }); - const newMetadata = [...filteredOldMetadata, ...filteredNewMetadata]; const updatedDSO = Object.assign({}, this.dso, { - metadata: newMetadata, + metadata: { + ...this.dso.metadata, + ...formMetadata + }, type: ResourceType.Community }); this.submitForm.emit(updatedDSO); diff --git a/src/app/shared/object-collection/shared/object-collection-element/abstract-listable-element.component.ts b/src/app/shared/object-collection/shared/object-collection-element/abstract-listable-element.component.ts index 59df86fdff..d52036b5dc 100644 --- a/src/app/shared/object-collection/shared/object-collection-element/abstract-listable-element.component.ts +++ b/src/app/shared/object-collection/shared/object-collection-element/abstract-listable-element.component.ts @@ -1,6 +1,5 @@ import { Component, Inject } from '@angular/core'; import { ListableObject } from '../listable-object.model'; -import { hasValue } from '../../../empty.util'; @Component({ selector: 'ds-abstract-object-element', @@ -11,8 +10,4 @@ export class AbstractListableElementComponent { public constructor(@Inject('objectElementProvider') public listableObject: ListableObject) { this.object = listableObject as T; } - - hasValue(data) { - return hasValue(data); - } } diff --git a/src/app/shared/object-grid/item-grid-element/item-grid-element.component.html b/src/app/shared/object-grid/item-grid-element/item-grid-element.component.html index 728dba7549..6bb2cfa99d 100644 --- a/src/app/shared/object-grid/item-grid-element/item-grid-element.component.html +++ b/src/app/shared/object-grid/item-grid-element/item-grid-element.component.html @@ -5,19 +5,19 @@
-

{{object.findMetadata('dc.title')}}

+

{{object.firstMetadataValue('dc.title')}}

-

- {{authorMd.value}} +

+ {{author}} ; - {{object.findMetadata("dc.date.issued")}} + {{object.firstMetadataValue("dc.date.issued")}}

-

{{object.findMetadata("dc.description.abstract") }}

+

{{object.firstMetadataValue("dc.description.abstract")}}

diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.html b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.html index 1e4f6f3c64..c7e2f524f3 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.html +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.html @@ -8,20 +8,20 @@
-

+

-

- {{dso.findMetadata("dc.date.issued")}} - , - + {{dso.firstMetadataValue('dc.date.issued')}} + , +

- +

diff --git a/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts b/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts index 5fd1c87edd..c3a80604db 100644 --- a/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts +++ b/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts @@ -2,12 +2,11 @@ import { Component, Inject } from '@angular/core'; import { SearchResult } from '../../../+search-page/search-result.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; -import { Metadatum } from '../../../core/shared/metadatum.model'; -import { isEmpty, hasNoValue, hasValue } from '../../empty.util'; import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; import { ListableObject } from '../../object-collection/shared/listable-object.model'; import { TruncatableService } from '../../truncatable/truncatable.service'; import { Observable } from 'rxjs'; +import { Metadata } from '../../../core/shared/metadata.model'; @Component({ selector: 'ds-search-result-grid-element', @@ -22,39 +21,14 @@ export class SearchResultGridElementComponent, K exten this.dso = this.object.dspaceObject; } - getValues(keys: string[]): string[] { - const results: string[] = new Array(); - this.object.hitHighlights.forEach( - (md: Metadatum) => { - if (keys.indexOf(md.key) > -1) { - results.push(md.value); - } - } - ); - if (isEmpty(results)) { - this.dso.filterMetadata(keys).forEach( - (md: Metadatum) => { - results.push(md.value); - } - ); - } - return results; + /** Gets all matching metadata string values from hitHighlights or dso metadata, preferring hitHighlights. */ + allMetadataValues(keyOrKeys: string | string[]): string[] { + return Metadata.allValues([this.object.hitHighlights, this.dso.metadata], keyOrKeys); } - getFirstValue(key: string): string { - let result: string; - this.object.hitHighlights.some( - (md: Metadatum) => { - if (key === md.key) { - result = md.value; - return true; - } - } - ); - if (hasNoValue(result)) { - result = this.dso.findMetadata(key); - } - return result; + /** Gets the first matching metadata string value from hitHighlights or dso metadata, preferring hitHighlights. */ + firstMetadataValue(keyOrKeys: string | string[]): string { + return Metadata.firstValue([this.object.hitHighlights, this.dso.metadata], keyOrKeys); } isCollapsed(): Observable { diff --git a/src/app/shared/object-list/item-list-element/item-list-element.component.html b/src/app/shared/object-list/item-list-element/item-list-element.component.html index 711ce19037..8179b77629 100644 --- a/src/app/shared/object-list/item-list-element/item-list-element.component.html +++ b/src/app/shared/object-list/item-list-element/item-list-element.component.html @@ -1,23 +1,23 @@ - {{object.findMetadata("dc.title")}} + {{object.firstMetadataValue("dc.title")}}
- - {{authorMd.value}} + {{author}} ; - ({{object.findMetadata("dc.publisher")}}, {{object.findMetadata("dc.date.issued")}}) + ({{object.firstMetadataValue("dc.publisher")}}, {{object.firstMetadataValue("dc.date.issued")}}) -
- {{object.findMetadata("dc.description.abstract")}} +
+ {{object.firstMetadataValue("dc.description.abstract")}}
diff --git a/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.html b/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.html index be549b2b76..b4af631e83 100644 --- a/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.html +++ b/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.html @@ -1,2 +1,2 @@ - -
+ +
diff --git a/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.html b/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.html index 150ca503cc..9444a63771 100644 --- a/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.html +++ b/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.html @@ -1,2 +1,2 @@ - -
+ +
diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.html b/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.html index 584d476e73..6261220459 100644 --- a/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.html +++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.html @@ -1,24 +1,24 @@ + [innerHTML]="firstMetadataValue('dc.title')"> - () - ) + - + -
+
+ [innerHTML]="firstMetadataValue('dc.description.abstract')">
- \ No newline at end of file + diff --git a/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts b/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts index 6a3b698dd6..a9a3050ac8 100644 --- a/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts +++ b/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts @@ -3,11 +3,10 @@ import { Observable } from 'rxjs'; import { SearchResult } from '../../../+search-page/search-result.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; -import { Metadatum } from '../../../core/shared/metadatum.model'; -import { hasNoValue, isEmpty } from '../../empty.util'; import { ListableObject } from '../../object-collection/shared/listable-object.model'; import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; import { TruncatableService } from '../../truncatable/truncatable.service'; +import { Metadata } from '../../../core/shared/metadata.model'; @Component({ selector: 'ds-search-result-list-element', @@ -22,39 +21,14 @@ export class SearchResultListElementComponent, K exten this.dso = this.object.dspaceObject; } - getValues(keys: string[]): string[] { - const results: string[] = new Array(); - this.object.hitHighlights.forEach( - (md: Metadatum) => { - if (keys.indexOf(md.key) > -1) { - results.push(md.value); - } - } - ); - if (isEmpty(results)) { - this.dso.filterMetadata(keys).forEach( - (md: Metadatum) => { - results.push(md.value); - } - ); - } - return results; + /** Gets all matching metadata string values from hitHighlights or dso metadata, preferring hitHighlights. */ + allMetadataValues(keyOrKeys: string | string[]): string[] { + return Metadata.allValues([this.object.hitHighlights, this.dso.metadata], keyOrKeys); } - getFirstValue(key: string): string { - let result: string; - this.object.hitHighlights.some( - (md: Metadatum) => { - if (key === md.key) { - result = md.value; - return true; - } - } - ); - if (hasNoValue(result)) { - result = this.dso.findMetadata(key); - } - return result; + /** Gets the first matching metadata string value from hitHighlights or dso metadata, preferring hitHighlights. */ + firstMetadataValue(keyOrKeys: string | string[]): string { + return Metadata.firstValue([this.object.hitHighlights, this.dso.metadata], keyOrKeys); } isCollapsed(): Observable {