diff --git a/src/app/item-page/item-page.module.ts b/src/app/item-page/item-page.module.ts
index 26040cfcb18..dba86ff8142 100644
--- a/src/app/item-page/item-page.module.ts
+++ b/src/app/item-page/item-page.module.ts
@@ -92,6 +92,7 @@ import { ClarinDateItemFieldComponent } from './simple/field-components/clarin-d
import { ClarinDescriptionItemFieldComponent } from './simple/field-components/clarin-description-item-field/clarin-description-item-field.component';
import { ClarinFilesSectionComponent } from './clarin-files-section/clarin-files-section.component';
import { UsageReportDataService } from '../core/statistics/usage-report-data.service';
+import { CreativeCommonsLicenseFieldComponent } from './simple/field-components/creative-commons-license-field/creative-commons-license-field.component';
const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator
@@ -157,7 +158,8 @@ const DECLARATIONS = [
ClarinIdentifierItemFieldComponent,
ClarinDateItemFieldComponent,
ClarinDescriptionItemFieldComponent,
- ClarinFilesSectionComponent
+ ClarinFilesSectionComponent,
+ CreativeCommonsLicenseFieldComponent
];
@NgModule({
diff --git a/src/app/item-page/simple/field-components/creative-commons-license-field/creative-commons-license-field.component.html b/src/app/item-page/simple/field-components/creative-commons-license-field/creative-commons-license-field.component.html
new file mode 100644
index 00000000000..21566fb02aa
--- /dev/null
+++ b/src/app/item-page/simple/field-components/creative-commons-license-field/creative-commons-license-field.component.html
@@ -0,0 +1,66 @@
+
+
+
+
{{ 'item.page.cc-license' | translate }}
+
+
+
diff --git a/src/app/item-page/simple/field-components/creative-commons-license-field/creative-commons-license-field.component.scss b/src/app/item-page/simple/field-components/creative-commons-license-field/creative-commons-license-field.component.scss
new file mode 100644
index 00000000000..57e9f8102e0
--- /dev/null
+++ b/src/app/item-page/simple/field-components/creative-commons-license-field/creative-commons-license-field.component.scss
@@ -0,0 +1,113 @@
+@import '../../item-page.component';
+
+@media (min-width: 992px) {
+ .col-2-5 {
+ flex: 0 0 20%;
+ max-width: 20%;
+ }
+}
+
+.clarin-item-page-field {
+ .cc-license-content {
+ .cc-license-row {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ }
+
+ .cc-license-link {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ text-decoration: none;
+ color: #007bff;
+ font-weight: 500;
+ transition: all 0.2s ease;
+
+ &:hover {
+ color: #0056b3;
+ text-decoration: none;
+ }
+
+ .cc-icons {
+ display: flex;
+ align-items: center;
+ gap: 2px;
+
+ i {
+ font-size: 1.1rem;
+
+ &.fa-creative-commons {
+ color: #f39c12;
+ }
+
+ &.fa-creative-commons-by {
+ color: #3498db;
+ }
+
+ &.fa-creative-commons-sa {
+ color: #27ae60;
+ }
+
+ &.fa-creative-commons-nc {
+ color: #e74c3c;
+ }
+
+ &.fa-creative-commons-nd {
+ color: #9b59b6;
+ }
+
+ &.fa-creative-commons-zero {
+ color: #34495e;
+ }
+ }
+ }
+
+ .cc-license-text {
+ font-size: 0.95rem;
+ font-weight: 500;
+ }
+
+ &:hover .cc-icons i {
+ font-size: 1.2rem;
+ }
+ }
+
+ .cc-license-description {
+ font-size: 0.85rem;
+ color: #6c757d;
+ font-style: italic;
+ margin-left: 0;
+ }
+ }
+}
+
+// Responsive design
+@media (max-width: 991.98px) {
+ .clarin-item-page-field {
+ .cc-license-content {
+ .cc-license-link {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 4px;
+
+ .cc-icons {
+ gap: 1px;
+
+ i {
+ font-size: 1rem;
+ }
+ }
+
+ .cc-license-text {
+ font-size: 0.9rem;
+ }
+ }
+
+ .cc-license-description {
+ font-size: 0.8rem;
+ margin-top: 2px;
+ }
+ }
+ }
+}
diff --git a/src/app/item-page/simple/field-components/creative-commons-license-field/creative-commons-license-field.component.spec.ts b/src/app/item-page/simple/field-components/creative-commons-license-field/creative-commons-license-field.component.spec.ts
new file mode 100644
index 00000000000..a89dd992f0a
--- /dev/null
+++ b/src/app/item-page/simple/field-components/creative-commons-license-field/creative-commons-license-field.component.spec.ts
@@ -0,0 +1,384 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { DebugElement } from '@angular/core';
+import { TranslateModule } from '@ngx-translate/core';
+import { of } from 'rxjs';
+
+import { CreativeCommonsLicenseFieldComponent } from './creative-commons-license-field.component';
+import { BundleDataService } from '../../../../core/data/bundle-data.service';
+import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
+import { Item } from '../../../../core/shared/item.model';
+import { RemoteData } from '../../../../core/data/remote-data';
+import { Bundle } from '../../../../core/shared/bundle.model';
+import { PaginatedList, buildPaginatedList } from '../../../../core/data/paginated-list.model';
+import { Bitstream } from '../../../../core/shared/bitstream.model';
+import { createSuccessfulRemoteDataObject, createFailedRemoteDataObject } from '../../../../shared/remote-data.utils';
+import { NoopAnimationsModule } from '@angular/platform-browser/animations';
+import { PageInfo } from '../../../../core/shared/page-info.model';
+
+describe('CreativeCommonsLicenseFieldComponent', () => {
+ let component: CreativeCommonsLicenseFieldComponent;
+ let fixture: ComponentFixture;
+ let bundleDataService: jasmine.SpyObj;
+ let bitstreamDataService: jasmine.SpyObj;
+
+ const mockItem = {
+ uuid: 'test-item-uuid',
+ metadata: {
+ 'dc.rights.uri': [{ value: 'https://creativecommons.org/licenses/by/4.0/' }],
+ 'dc.rights': [{ value: 'Attribution 4.0 International' }]
+ },
+ allMetadata: jasmine.createSpy('allMetadata').and.callFake((field: string) => {
+ return mockItem.metadata[field] || [];
+ })
+ } as any as Item;
+
+ const mockItemWithoutLicense = {
+ uuid: 'test-item-uuid-no-license',
+ metadata: {
+ 'dc.title': [{ value: 'Test Item' }]
+ },
+ allMetadata: jasmine.createSpy('allMetadata').and.callFake((field: string) => {
+ return mockItemWithoutLicense.metadata[field] || [];
+ })
+ } as any as Item;
+
+ const mockBundle = {
+ uuid: 'test-bundle-uuid',
+ name: 'CC_LICENSE'
+ } as Bundle;
+
+ const mockBitstream = {
+ uuid: 'test-bitstream-uuid',
+ name: 'license.txt',
+ metadata: {
+ 'dc.identifier.uri': [{ value: 'https://creativecommons.org/licenses/by-nc-sa/3.0/' }]
+ }
+ } as any as Bitstream;
+
+ beforeEach(async () => {
+ const bundleDataServiceSpy = jasmine.createSpyObj('BundleDataService', ['findByItemAndName']);
+ const bitstreamDataServiceSpy = jasmine.createSpyObj('BitstreamDataService', ['findAllByItemAndBundleName']);
+
+ await TestBed.configureTestingModule({
+ declarations: [CreativeCommonsLicenseFieldComponent],
+ imports: [
+ TranslateModule.forRoot(),
+ NoopAnimationsModule
+ ],
+ providers: [
+ { provide: BundleDataService, useValue: bundleDataServiceSpy },
+ { provide: BitstreamDataService, useValue: bitstreamDataServiceSpy }
+ ]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(CreativeCommonsLicenseFieldComponent);
+ component = fixture.componentInstance;
+ bundleDataService = TestBed.inject(BundleDataService) as jasmine.SpyObj;
+ bitstreamDataService = TestBed.inject(BitstreamDataService) as jasmine.SpyObj;
+ });
+
+ describe('Component Initialization', () => {
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should initialize with item input', () => {
+ component.item = mockItem;
+ expect(component.item).toBe(mockItem);
+ });
+ });
+
+ describe('Creative Commons License Detection', () => {
+ beforeEach(() => {
+ component.item = mockItem;
+ });
+
+ it('should detect Creative Commons license from metadata', (done) => {
+ bundleDataService.findByItemAndName.and.returnValue(of(createFailedRemoteDataObject()));
+
+ component.ngOnInit();
+
+ component.hasCcLicense$.subscribe(hasLicense => {
+ expect(hasLicense).toBe(true);
+ done();
+ });
+ });
+
+ it('should detect Creative Commons license from CC_LICENSE bundle', (done) => {
+ bundleDataService.findByItemAndName.and.returnValue(of(createSuccessfulRemoteDataObject(mockBundle)));
+ bitstreamDataService.findAllByItemAndBundleName.and.returnValue(
+ of(createSuccessfulRemoteDataObject(buildPaginatedList(new PageInfo(), [mockBitstream])))
+ );
+
+ component.ngOnInit();
+
+ component.hasCcLicense$.subscribe(hasLicense => {
+ expect(hasLicense).toBe(true);
+ done();
+ });
+ });
+
+ it('should return false when no Creative Commons license is found', (done) => {
+ component.item = mockItemWithoutLicense;
+ bundleDataService.findByItemAndName.and.returnValue(of(createFailedRemoteDataObject()));
+
+ component.ngOnInit();
+
+ component.hasCcLicense$.subscribe(hasLicense => {
+ expect(hasLicense).toBe(false);
+ done();
+ });
+ });
+ });
+
+ describe('License URL Extraction', () => {
+ beforeEach(() => {
+ component.item = mockItem;
+ });
+
+ it('should extract license URL from metadata', (done) => {
+ bundleDataService.findByItemAndName.and.returnValue(of(createFailedRemoteDataObject()));
+
+ component.ngOnInit();
+
+ component.ccLicenseUrl$.subscribe(url => {
+ expect(url).toBe('https://creativecommons.org/licenses/by/4.0/');
+ done();
+ });
+ });
+
+ it('should extract license URL from bitstream metadata', (done) => {
+ bundleDataService.findByItemAndName.and.returnValue(of(createSuccessfulRemoteDataObject(mockBundle)));
+ bitstreamDataService.findAllByItemAndBundleName.and.returnValue(
+ of(createSuccessfulRemoteDataObject(buildPaginatedList(new PageInfo(), [mockBitstream])))
+ );
+
+ component.ngOnInit();
+
+ component.ccLicenseUrl$.subscribe(url => {
+ expect(url).toBe('https://creativecommons.org/licenses/by-nc-sa/3.0/');
+ done();
+ });
+ });
+
+ it('should return empty string when no license URL is found', (done) => {
+ component.item = mockItemWithoutLicense;
+ bundleDataService.findByItemAndName.and.returnValue(of(createFailedRemoteDataObject()));
+
+ component.ngOnInit();
+
+ component.ccLicenseUrl$.subscribe(url => {
+ expect(url).toBe('');
+ done();
+ });
+ });
+ });
+
+ describe('License Name Extraction', () => {
+ beforeEach(() => {
+ component.item = mockItem;
+ });
+
+ it('should extract license name from URL', (done) => {
+ bundleDataService.findByItemAndName.and.returnValue(of(createFailedRemoteDataObject()));
+
+ component.ngOnInit();
+
+ component.ccLicenseName$.subscribe(name => {
+ expect(name).toBe('CC BY 4.0');
+ done();
+ });
+ });
+
+ it('should return empty string when no license URL is available', (done) => {
+ component.item = mockItemWithoutLicense;
+ bundleDataService.findByItemAndName.and.returnValue(of(createFailedRemoteDataObject()));
+
+ component.ngOnInit();
+
+ component.ccLicenseName$.subscribe(name => {
+ expect(name).toBe('');
+ done();
+ });
+ });
+ });
+
+ describe('License Type Detection', () => {
+ const testCases = [
+ { input: 'CC BY 4.0', expected: 'by' },
+ { input: 'CC BY-SA 3.0', expected: 'by-sa' },
+ { input: 'CC BY-NC 2.0', expected: 'by-nc' },
+ { input: 'CC BY-NC-SA 4.0', expected: 'by-nc-sa' },
+ { input: 'CC BY-ND 3.0', expected: 'by-nd' },
+ { input: 'CC BY-NC-ND 2.0', expected: 'by-nc-nd' },
+ { input: 'CC0 1.0', expected: 'cc0' },
+ { input: 'Public Domain Mark', expected: 'cc0' },
+ { input: '', expected: '' },
+ { input: 'Unknown License', expected: '' }
+ ];
+
+ testCases.forEach(testCase => {
+ it(`should return '${testCase.expected}' for license '${testCase.input}'`, () => {
+ const result = component.getLicenseType(testCase.input);
+ expect(result).toBe(testCase.expected);
+ });
+ });
+ });
+
+ describe('License Name Extraction from URL', () => {
+ const urlTestCases = [
+ {
+ url: 'https://creativecommons.org/licenses/by/4.0/',
+ expected: 'CC BY 4.0'
+ },
+ {
+ url: 'https://creativecommons.org/licenses/by-sa/3.0/',
+ expected: 'CC BY-SA 3.0'
+ },
+ {
+ url: 'https://creativecommons.org/licenses/by-nc/2.0/',
+ expected: 'CC BY-NC 2.0'
+ },
+ {
+ url: 'https://creativecommons.org/licenses/by-nc-sa/4.0/',
+ expected: 'CC BY-NC-SA 4.0'
+ },
+ {
+ url: 'https://creativecommons.org/licenses/by-nd/3.0/',
+ expected: 'CC BY-ND 3.0'
+ },
+ {
+ url: 'https://creativecommons.org/licenses/by-nc-nd/2.0/',
+ expected: 'CC BY-NC-ND 2.0'
+ },
+ {
+ url: 'https://creativecommons.org/publicdomain/zero/1.0/',
+ expected: 'CC0 1.0'
+ },
+ {
+ url: 'https://creativecommons.org/publicdomain/mark/1.0/',
+ expected: 'Public Domain 1.0'
+ },
+ {
+ url: 'https://example.com/not-cc-license',
+ expected: ''
+ },
+ {
+ url: '',
+ expected: ''
+ }
+ ];
+
+ urlTestCases.forEach(testCase => {
+ it(`should extract '${testCase.expected}' from URL '${testCase.url}'`, () => {
+ const result = component['extractLicenseNameFromUrl'](testCase.url);
+ expect(result).toBe(testCase.expected);
+ });
+ });
+ });
+
+ describe('Metadata Extraction', () => {
+ it('should extract Creative Commons URL from dc.rights.uri', () => {
+ component.item = mockItem;
+ const result = component['extractUrlFromMetadata']();
+ expect(result).toBe('https://creativecommons.org/licenses/by/4.0/');
+ });
+
+ it('should return empty string when no Creative Commons URL is found in metadata', () => {
+ component.item = mockItemWithoutLicense;
+ const result = component['extractUrlFromMetadata']();
+ expect(result).toBe('');
+ });
+
+ it('should check multiple metadata fields for Creative Commons URL', () => {
+ const itemWithDifferentField = {
+ uuid: 'test-item-uuid',
+ metadata: {
+ 'dc.rights': [{ value: 'https://creativecommons.org/licenses/by-sa/3.0/' }]
+ },
+ allMetadata: jasmine.createSpy('allMetadata').and.callFake((field: string) => {
+ return itemWithDifferentField.metadata[field] || [];
+ })
+ } as any as Item;
+
+ component.item = itemWithDifferentField;
+ const result = component['extractUrlFromMetadata']();
+ expect(result).toBe('https://creativecommons.org/licenses/by-sa/3.0/');
+ });
+ });
+
+ describe('Component Template Integration', () => {
+ it('should not display license field when no license is present', () => {
+ component.item = mockItemWithoutLicense;
+ bundleDataService.findByItemAndName.and.returnValue(of(createFailedRemoteDataObject()));
+
+ component.ngOnInit();
+ fixture.detectChanges();
+
+ const licenseElement = fixture.debugElement.query(By.css('.clarin-item-page-field'));
+ expect(licenseElement).toBeNull();
+ });
+
+ it('should display license field when Creative Commons license is present', () => {
+ component.item = mockItem;
+ bundleDataService.findByItemAndName.and.returnValue(of(createFailedRemoteDataObject()));
+
+ component.ngOnInit();
+ fixture.detectChanges();
+
+ setTimeout(() => {
+ fixture.detectChanges();
+ const licenseElement = fixture.debugElement.query(By.css('.clarin-item-page-field'));
+ expect(licenseElement).toBeTruthy();
+ }, 100);
+ });
+ });
+
+ describe('Error Handling', () => {
+ it('should handle bundle service errors gracefully', (done) => {
+ component.item = mockItem;
+ bundleDataService.findByItemAndName.and.returnValue(of(createFailedRemoteDataObject()));
+
+ component.ngOnInit();
+
+ component.hasCcLicense$.subscribe(hasLicense => {
+ expect(hasLicense).toBe(true); // Should still detect from metadata
+ done();
+ });
+ });
+
+ it('should handle bitstream service errors gracefully', (done) => {
+ component.item = mockItem;
+ bundleDataService.findByItemAndName.and.returnValue(of(createSuccessfulRemoteDataObject(mockBundle)));
+ bitstreamDataService.findAllByItemAndBundleName.and.returnValue(of(createFailedRemoteDataObject>()));
+
+ component.ngOnInit();
+
+ component.ccLicenseUrl$.subscribe(url => {
+ expect(url).toBe('https://creativecommons.org/licenses/by/4.0/'); // Fallback to metadata
+ done();
+ });
+ });
+ });
+
+ describe('Debug Methods', () => {
+ it('should return metadata values for debug purposes', () => {
+ component.item = mockItem;
+ const result = component.getMetadataValues('dc.rights.uri');
+ expect(result).toBe('https://creativecommons.org/licenses/by/4.0/');
+ });
+
+ it('should return "No values found" when metadata field does not exist', () => {
+ component.item = mockItem;
+ const result = component.getMetadataValues('nonexistent.field');
+ expect(result).toBe('No values found');
+ });
+
+ it('should return "No metadata available" when item has no metadata', () => {
+ component.item = { metadata: null } as any as Item;
+ const result = component.getMetadataValues('dc.rights.uri');
+ expect(result).toBe('No metadata available');
+ });
+ });
+});
diff --git a/src/app/item-page/simple/field-components/creative-commons-license-field/creative-commons-license-field.component.ts b/src/app/item-page/simple/field-components/creative-commons-license-field/creative-commons-license-field.component.ts
new file mode 100644
index 00000000000..fec3ac0ca1d
--- /dev/null
+++ b/src/app/item-page/simple/field-components/creative-commons-license-field/creative-commons-license-field.component.ts
@@ -0,0 +1,257 @@
+import { Component, Input, OnInit } from '@angular/core';
+import { Observable, of } from 'rxjs';
+import { map, switchMap, catchError } from 'rxjs/operators';
+import { Item } from '../../../../core/shared/item.model';
+import { BundleDataService } from '../../../../core/data/bundle-data.service';
+import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
+import { getAllSucceededRemoteDataPayload, getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators';
+import { RemoteData } from '../../../../core/data/remote-data';
+import { Bundle } from '../../../../core/shared/bundle.model';
+import { PaginatedList } from '../../../../core/data/paginated-list.model';
+import { Bitstream } from '../../../../core/shared/bitstream.model';
+import { isNotEmpty } from '../../../../shared/empty.util';
+
+/**
+ * Constants for Creative Commons license handling
+ */
+const CC_CONSTANTS = {
+ BUNDLE_NAME: 'CC_LICENSE',
+ DOMAIN: 'creativecommons.org',
+ METADATA_FIELDS: [
+ 'dc.rights.uri',
+ 'dc.rights',
+ 'dc.identifier.uri'
+ ],
+ LICENSE_TYPES: {
+ 'by/': 'CC BY',
+ 'by-sa/': 'CC BY-SA',
+ 'by-nc/': 'CC BY-NC',
+ 'by-nc-sa/': 'CC BY-NC-SA',
+ 'by-nd/': 'CC BY-ND',
+ 'by-nc-nd/': 'CC BY-NC-ND',
+ 'publicdomain/zero/': 'CC0',
+ 'publicdomain/mark/': 'Public Domain'
+ },
+ LICENSE_TYPE_PATTERNS: {
+ CC0: ['cc0', 'public domain'],
+ BY_NC_ND: ['by-nc-nd'],
+ BY_NC_SA: ['by-nc-sa'],
+ BY_NC: ['by-nc'],
+ BY_ND: ['by-nd'],
+ BY_SA: ['by-sa'],
+ BY: ['by']
+ },
+ ICON_CLASSES: {
+ BASE: 'fab fa-creative-commons',
+ BY: 'fab fa-creative-commons-by',
+ SA: 'fab fa-creative-commons-sa',
+ NC: 'fab fa-creative-commons-nc',
+ ND: 'fab fa-creative-commons-nd',
+ ZERO: 'fab fa-creative-commons-zero'
+ },
+ TEMPLATE_SWITCH_CASES: {
+ CC0: 'cc0',
+ BY_NC_ND: 'by-nc-nd',
+ BY_NC_SA: 'by-nc-sa',
+ BY_NC: 'by-nc',
+ BY_ND: 'by-nd',
+ BY_SA: 'by-sa',
+ BY: 'by'
+ },
+ DEFAULT_MESSAGES: {
+ NO_METADATA: 'No metadata available',
+ NO_VALUES: 'No values found',
+ DEFAULT_LICENSE: 'Creative Commons License'
+ }
+} as const;
+
+/**
+ * Component for displaying Creative Commons license information on item pages
+ */
+@Component({
+ selector: 'ds-creative-commons-license-field',
+ templateUrl: './creative-commons-license-field.component.html',
+ styleUrls: ['./creative-commons-license-field.component.scss']
+})
+export class CreativeCommonsLicenseFieldComponent implements OnInit {
+
+ /**
+ * The item to display Creative Commons license for
+ */
+ @Input() item: Item;
+
+ /**
+ * Creative Commons license URL
+ */
+ ccLicenseUrl$: Observable;
+
+ /**
+ * Creative Commons license name
+ */
+ ccLicenseName$: Observable;
+
+ /**
+ * Whether the item has a Creative Commons license
+ */
+ hasCcLicense$: Observable;
+
+ constructor(
+ private bundleService: BundleDataService,
+ private bitstreamService: BitstreamDataService
+ ) { }
+
+ ngOnInit(): void {
+ this.initializeCcLicense();
+ }
+
+ /**
+ * Initialize Creative Commons license information
+ */
+ private initializeCcLicense(): void {
+ // Cache the metadata URL extraction to avoid repeated calls
+ const metadataUrl = this.extractUrlFromMetadata();
+
+ // Check if item has CC_LICENSE bundle and extract license information
+ const ccLicenseBundle$ = this.bundleService.findByItemAndName(this.item, CC_CONSTANTS.BUNDLE_NAME);
+
+ this.hasCcLicense$ = ccLicenseBundle$.pipe(
+ map((bundleRD: RemoteData) => {
+ // Check if CC_LICENSE bundle exists OR if CC license metadata exists
+ const hasBundleLicense = bundleRD.hasSucceeded && isNotEmpty(bundleRD.payload);
+ const hasMetadataLicense = isNotEmpty(metadataUrl);
+ return hasBundleLicense || hasMetadataLicense;
+ }),
+ catchError(() => of(false))
+ );
+
+ // Get the license URL from bitstreams in CC_LICENSE bundle
+ this.ccLicenseUrl$ = ccLicenseBundle$.pipe(
+ switchMap((bundleRD: RemoteData) => {
+ if (bundleRD.hasSucceeded && isNotEmpty(bundleRD.payload)) {
+ return this.bitstreamService.findAllByItemAndBundleName(this.item, CC_CONSTANTS.BUNDLE_NAME);
+ }
+ return of(null);
+ }),
+ switchMap((bitstreamsRD) => {
+ if (bitstreamsRD && bitstreamsRD.hasSucceeded && isNotEmpty(bitstreamsRD.payload)) {
+ const bitstreams = bitstreamsRD.payload;
+ if (bitstreams.page.length > 0) {
+ // Look for license URL in bitstream metadata or name
+ const licenseBitstream = bitstreams.page.find(bitstream =>
+ bitstream.name.includes('license') ||
+ bitstream.metadata[CC_CONSTANTS.METADATA_FIELDS[0]]?.[0]?.value
+ );
+ const url = licenseBitstream?.metadata[CC_CONSTANTS.METADATA_FIELDS[0]]?.[0]?.value ||
+ metadataUrl ||
+ '';
+ return of(url);
+ }
+ }
+ // Fallback to cached metadata-based detection
+ return of(metadataUrl);
+ }),
+ catchError(() => of(''))
+ );
+
+ // Extract license name from URL or metadata
+ this.ccLicenseName$ = this.ccLicenseUrl$.pipe(
+ map((url: string) => {
+ if (isNotEmpty(url)) {
+ return this.extractLicenseNameFromUrl(url);
+ }
+ return '';
+ })
+ );
+ }
+
+ /**
+ * Extract Creative Commons license URL from item metadata
+ */
+ private extractUrlFromMetadata(): string {
+ // Check for common CC license metadata fields
+ for (const field of CC_CONSTANTS.METADATA_FIELDS) {
+ const values = this.item.allMetadata(field);
+ if (values) {
+ for (const value of values) {
+ if (value.value && value.value.includes(CC_CONSTANTS.DOMAIN)) {
+ return value.value;
+ }
+ }
+ }
+ }
+ return '';
+ }
+
+ /**
+ * Extract license name from Creative Commons URL
+ */
+ private extractLicenseNameFromUrl(url: string): string {
+ if (!url || !url.includes(CC_CONSTANTS.DOMAIN)) {
+ return '';
+ }
+
+ // Parse common CC license types from URL
+ for (const [pattern, name] of Object.entries(CC_CONSTANTS.LICENSE_TYPES)) {
+ if (url.includes(pattern)) {
+ // Extract version if present
+ const versionMatch = url.match(/(\d+\.\d+)/);
+ const version = versionMatch ? ` ${versionMatch[1]}` : '';
+ return `${name}${version}`;
+ }
+ }
+
+ return CC_CONSTANTS.DEFAULT_MESSAGES.DEFAULT_LICENSE;
+ }
+
+ /**
+ * Get Creative Commons icon class based on license type
+ */
+ getCcIconClass(licenseName: string): string {
+ const lowerName = licenseName.toLowerCase();
+
+ if (CC_CONSTANTS.LICENSE_TYPE_PATTERNS.CC0.some(pattern => lowerName.includes(pattern))) {
+ return CC_CONSTANTS.ICON_CLASSES.ZERO;
+ } else if (CC_CONSTANTS.LICENSE_TYPE_PATTERNS.BY_NC_ND.some(pattern => lowerName.includes(pattern))) {
+ return `${CC_CONSTANTS.ICON_CLASSES.BASE} ${CC_CONSTANTS.ICON_CLASSES.BY} ${CC_CONSTANTS.ICON_CLASSES.NC} ${CC_CONSTANTS.ICON_CLASSES.ND}`;
+ } else if (CC_CONSTANTS.LICENSE_TYPE_PATTERNS.BY_NC_SA.some(pattern => lowerName.includes(pattern))) {
+ return `${CC_CONSTANTS.ICON_CLASSES.BASE} ${CC_CONSTANTS.ICON_CLASSES.BY} ${CC_CONSTANTS.ICON_CLASSES.NC} ${CC_CONSTANTS.ICON_CLASSES.SA}`;
+ } else if (CC_CONSTANTS.LICENSE_TYPE_PATTERNS.BY_NC.some(pattern => lowerName.includes(pattern))) {
+ return `${CC_CONSTANTS.ICON_CLASSES.BASE} ${CC_CONSTANTS.ICON_CLASSES.BY} ${CC_CONSTANTS.ICON_CLASSES.NC}`;
+ } else if (CC_CONSTANTS.LICENSE_TYPE_PATTERNS.BY_ND.some(pattern => lowerName.includes(pattern))) {
+ return `${CC_CONSTANTS.ICON_CLASSES.BASE} ${CC_CONSTANTS.ICON_CLASSES.BY} ${CC_CONSTANTS.ICON_CLASSES.ND}`;
+ } else if (CC_CONSTANTS.LICENSE_TYPE_PATTERNS.BY_SA.some(pattern => lowerName.includes(pattern))) {
+ return `${CC_CONSTANTS.ICON_CLASSES.BASE} ${CC_CONSTANTS.ICON_CLASSES.BY} ${CC_CONSTANTS.ICON_CLASSES.SA}`;
+ } else if (CC_CONSTANTS.LICENSE_TYPE_PATTERNS.BY.some(pattern => lowerName.includes(pattern))) {
+ return `${CC_CONSTANTS.ICON_CLASSES.BASE} ${CC_CONSTANTS.ICON_CLASSES.BY}`;
+ }
+
+ return CC_CONSTANTS.ICON_CLASSES.BASE;
+ }
+
+ /**
+ * Get license type for switch case in template
+ */
+ getLicenseType(licenseName: string): string {
+ if (!licenseName) return '';
+
+ const name = licenseName.toLowerCase();
+
+ if (CC_CONSTANTS.LICENSE_TYPE_PATTERNS.CC0.some(pattern => name.includes(pattern))) {
+ return CC_CONSTANTS.TEMPLATE_SWITCH_CASES.CC0;
+ } else if (CC_CONSTANTS.LICENSE_TYPE_PATTERNS.BY_NC_ND.some(pattern => name.includes(pattern))) {
+ return CC_CONSTANTS.TEMPLATE_SWITCH_CASES.BY_NC_ND;
+ } else if (CC_CONSTANTS.LICENSE_TYPE_PATTERNS.BY_NC_SA.some(pattern => name.includes(pattern))) {
+ return CC_CONSTANTS.TEMPLATE_SWITCH_CASES.BY_NC_SA;
+ } else if (CC_CONSTANTS.LICENSE_TYPE_PATTERNS.BY_NC.some(pattern => name.includes(pattern))) {
+ return CC_CONSTANTS.TEMPLATE_SWITCH_CASES.BY_NC;
+ } else if (CC_CONSTANTS.LICENSE_TYPE_PATTERNS.BY_ND.some(pattern => name.includes(pattern))) {
+ return CC_CONSTANTS.TEMPLATE_SWITCH_CASES.BY_ND;
+ } else if (CC_CONSTANTS.LICENSE_TYPE_PATTERNS.BY_SA.some(pattern => name.includes(pattern))) {
+ return CC_CONSTANTS.TEMPLATE_SWITCH_CASES.BY_SA;
+ } else if (CC_CONSTANTS.LICENSE_TYPE_PATTERNS.BY.some(pattern => name.includes(pattern))) {
+ return CC_CONSTANTS.TEMPLATE_SWITCH_CASES.BY;
+ } else {
+ return '';
+ }
+ }
+}
diff --git a/src/app/item-page/simple/item-types/publication/publication.component.html b/src/app/item-page/simple/item-types/publication/publication.component.html
index 5c00b504060..dd53479c46c 100644
--- a/src/app/item-page/simple/item-types/publication/publication.component.html
+++ b/src/app/item-page/simple/item-types/publication/publication.component.html
@@ -94,6 +94,7 @@
[fields]="['dc.identifier.uri']"
[label]="'item.page.uri'">
+