add language file fallback method

This commit is contained in:
amentis 2023-11-28 19:10:04 +02:00
parent 3d5c718d52
commit 126d47f9e4
15 changed files with 61 additions and 309 deletions

View File

@ -20,7 +20,7 @@ public class LanguageEntity {
private String code; private String code;
public static final String _code = "code"; public static final String _code = "code";
@Column(name = "payload", nullable = false) @Column(name = "payload")
private String payload; private String payload;
public static final String _payload = "payload"; public static final String _payload = "payload";

View File

@ -16,8 +16,6 @@ public class LanguagePersist {
@Size(max = 20, message = "{validation.largerthanmax}") @Size(max = 20, message = "{validation.largerthanmax}")
private String code; private String code;
@NotNull(message = "{validation.empty}")
@NotEmpty(message = "{validation.empty}")
private String payload; private String payload;
private String hash; private String hash;

View File

@ -9,11 +9,14 @@ import gr.cite.tools.exception.MyValidationException;
import gr.cite.tools.fieldset.FieldSet; import gr.cite.tools.fieldset.FieldSet;
import javax.management.InvalidApplicationException; import javax.management.InvalidApplicationException;
import java.io.IOException;
import java.util.UUID; import java.util.UUID;
public interface LanguageService { public interface LanguageService {
Language persist(LanguagePersist model, FieldSet fields) throws MyForbiddenException, MyValidationException, MyApplicationException, MyNotFoundException, InvalidApplicationException; Language persist(LanguagePersist model, FieldSet fields) throws MyForbiddenException, MyValidationException, MyApplicationException, MyNotFoundException, InvalidApplicationException;
String getPayload(String code) throws IOException;
void deleteAndSave(UUID id) throws MyForbiddenException, InvalidApplicationException; void deleteAndSave(UUID id) throws MyForbiddenException, InvalidApplicationException;
} }

View File

@ -26,9 +26,16 @@ import jakarta.persistence.EntityManager;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.context.MessageSource; import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.management.InvalidApplicationException; import javax.management.InvalidApplicationException;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.Instant; import java.time.Instant;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@ -44,11 +51,12 @@ public class LanguageServiceImpl implements LanguageService {
private final ConventionService conventionService; private final ConventionService conventionService;
private final MessageSource messageSource; private final MessageSource messageSource;
private final ErrorThesaurusProperties errors; private final ErrorThesaurusProperties errors;
private final Environment environment;
public LanguageServiceImpl( public LanguageServiceImpl(
EntityManager entityManager, AuthorizationService authorizationService, DeleterFactory deleterFactory, BuilderFactory builderFactory, EntityManager entityManager, AuthorizationService authorizationService, DeleterFactory deleterFactory, BuilderFactory builderFactory,
ConventionService conventionService, MessageSource messageSource, ErrorThesaurusProperties errors){ ConventionService conventionService, MessageSource messageSource, ErrorThesaurusProperties errors, Environment environment){
this.entityManager = entityManager; this.entityManager = entityManager;
this.authorizationService = authorizationService; this.authorizationService = authorizationService;
this.deleterFactory = deleterFactory; this.deleterFactory = deleterFactory;
@ -56,6 +64,7 @@ public class LanguageServiceImpl implements LanguageService {
this.conventionService = conventionService; this.conventionService = conventionService;
this.messageSource = messageSource; this.messageSource = messageSource;
this.errors = errors; this.errors = errors;
this.environment = environment;
} }
@ -92,6 +101,19 @@ public class LanguageServiceImpl implements LanguageService {
return this.builderFactory.builder(LanguageBuilder.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermissionOrPublic).build(BaseFieldSet.build(fields, Language._id), data); return this.builderFactory.builder(LanguageBuilder.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermissionOrPublic).build(BaseFieldSet.build(fields, Language._id), data);
} }
public String getPayload(String code) throws IOException {
this.authorizationService.authorizeForce(Permission.BrowseLanguage);
String fileName = this.environment.getProperty("language.path") + code + ".json";
InputStream is = new FileInputStream(fileName);
byte[] content = new byte[is.available()];
is.read(content);
is.close();
return new String(content, StandardCharsets.UTF_8);
}
public void deleteAndSave(UUID id) throws MyForbiddenException, InvalidApplicationException { public void deleteAndSave(UUID id) throws MyForbiddenException, InvalidApplicationException {
logger.debug("deleting : {}", id); logger.debug("deleting : {}", id);

View File

@ -3,6 +3,7 @@ package eu.eudat.controllers.v2;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import eu.eudat.audit.AuditableAction; import eu.eudat.audit.AuditableAction;
import eu.eudat.authorization.AuthorizationFlags; import eu.eudat.authorization.AuthorizationFlags;
import eu.eudat.authorization.Permission;
import eu.eudat.data.LanguageEntity; import eu.eudat.data.LanguageEntity;
import eu.eudat.model.Language; import eu.eudat.model.Language;
import eu.eudat.model.builder.LanguageBuilder; import eu.eudat.model.builder.LanguageBuilder;
@ -33,6 +34,7 @@ import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import javax.management.InvalidApplicationException; import javax.management.InvalidApplicationException;
import java.io.IOException;
import java.util.AbstractMap; import java.util.AbstractMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -50,7 +52,6 @@ public class LanguageV2Controller {
private final MessageSource messageSource; private final MessageSource messageSource;
private final AuthorizationService authorizationService; private final AuthorizationService authorizationService;
private final LanguageService languageService; private final LanguageService languageService;
@Autowired @Autowired
public LanguageV2Controller( public LanguageV2Controller(
BuilderFactory builderFactory, BuilderFactory builderFactory,
@ -104,16 +105,20 @@ public class LanguageV2Controller {
} }
@GetMapping("code/{code}") @GetMapping("code/{code}")
public Language get(@PathVariable("code") String code, FieldSet fieldSet) throws MyApplicationException, MyForbiddenException, MyNotFoundException { public Language get(@PathVariable("code") String code, FieldSet fieldSet) throws MyApplicationException, MyForbiddenException, MyNotFoundException, IOException {
logger.debug(new MapLogEntry("retrieving" + Language.class.getSimpleName()).And("code", code).And("fields", fieldSet)); logger.debug(new MapLogEntry("retrieving" + Language.class.getSimpleName()).And("code", code).And("fields", fieldSet));
this.censorFactory.censor(LanguageCensor.class).censor(fieldSet, null); this.censorFactory.censor(LanguageCensor.class).censor(fieldSet, null);
LanguageQuery query = this.queryFactory.query(LanguageQuery.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermissionOrPublic).codes(code); LanguageQuery query = this.queryFactory.query(LanguageQuery.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermissionOrPublic).codes(code);
Language model = this.builderFactory.builder(LanguageBuilder.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermissionOrPublic).build(fieldSet, query.firstAs(fieldSet)); Language model = this.builderFactory.builder(LanguageBuilder.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermissionOrPublic).build(fieldSet, query.firstAs(fieldSet));
if (model == null) if (model == null)
throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{code, Language.class.getSimpleName()}, LocaleContextHolder.getLocale())); throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{code, Language.class.getSimpleName()}, LocaleContextHolder.getLocale()));
if (model.getPayload() == null){
model.setPayload(this.languageService.getPayload(code));
}
this.auditService.track(AuditableAction.Language_Lookup, Map.ofEntries( this.auditService.track(AuditableAction.Language_Lookup, Map.ofEntries(
new AbstractMap.SimpleEntry<String, Object>("code", code), new AbstractMap.SimpleEntry<String, Object>("code", code),
new AbstractMap.SimpleEntry<String, Object>("fields", fieldSet) new AbstractMap.SimpleEntry<String, Object>("fields", fieldSet)

View File

@ -8,7 +8,7 @@ BEGIN
( (
id uuid NOT NULL, id uuid NOT NULL,
code character varying(20) NOT NULL, code character varying(20) NOT NULL,
payload text NOT NULL, payload text,
created_at timestamp without time zone NOT NULL, created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL, updated_at timestamp without time zone NOT NULL,
is_active smallint NOT NULL, is_active smallint NOT NULL,

View File

@ -35,6 +35,15 @@ export class LanguageV2Service {
catchError((error: any) => throwError(error))); catchError((error: any) => throwError(error)));
} }
getSingleWithCode(code: string, reqFields: string[] = []): Observable<Language> {
const url = `${this.apiBase}/code/${code}`;
const options = { params: { f: reqFields } };
return this.http
.get<Language>(url, options).pipe(
catchError((error: any) => throwError(error)));
}
persist(item: LanguagePersist): Observable<Language> { persist(item: LanguagePersist): Observable<Language> {
const url = `${this.apiBase}/persist`; const url = `${this.apiBase}/persist`;

View File

@ -37,10 +37,10 @@
{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error> {{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="col-4"> <div class="col-12">
<mat-form-field class="w-100"> <mat-form-field class="w-100">
<mat-label>{{'LANGUAGE-EDITOR.FIELDS.PAYLOAD' | translate}}</mat-label> <mat-label>{{'LANGUAGE-EDITOR.FIELDS.PAYLOAD' | translate}}</mat-label>
<input matInput type="text" name="payload" [formControl]="formGroup.get('payload')" required> <input matInput type="text" name="payload" [formControl]="formGroup.get('payload')">
<mat-error *ngIf="formGroup.get('payload').hasError('required')"> <mat-error *ngIf="formGroup.get('payload').hasError('required')">
{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error> {{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>
</mat-form-field> </mat-form-field>

View File

@ -43,8 +43,6 @@ export class LanguageEditorComponent extends BaseEditor<LanguageEditorModel, Lan
isDeleted = false; isDeleted = false;
formGroup: UntypedFormGroup = null; formGroup: UntypedFormGroup = null;
showInactiveDetails = false; showInactiveDetails = false;
depositCodes: string[] = [];
fileTransformersCodes: string[] = [];
protected get canDelete(): boolean { protected get canDelete(): boolean {
return !this.isDeleted && !this.isNew && this.hasPermission(this.authService.permissionEnum.DeleteLanguage); return !this.isDeleted && !this.isNew && this.hasPermission(this.authService.permissionEnum.DeleteLanguage);
@ -103,9 +101,20 @@ export class LanguageEditorComponent extends BaseEditor<LanguageEditorModel, Lan
prepareForm(data: Language) { prepareForm(data: Language) {
try { try {
this.editorModel = data ? new LanguageEditorModel().fromModel(data) : new LanguageEditorModel(); if(data && data.payload == null){
this.isDeleted = data ? data.isActive === IsActive.Inactive : false; this.languageV2Service.getSingleWithCode(data.code, LanguageEditorResolver.lookupFields())
this.buildForm(); .pipe(takeUntil(this._destroyed))
.subscribe(language => {
data.payload = language.payload;
this.editorModel = data ? new LanguageEditorModel().fromModel(data) : new LanguageEditorModel();
this.isDeleted = data ? data.isActive === IsActive.Inactive : false;
this.buildForm();
});
}else{
this.editorModel = data ? new LanguageEditorModel().fromModel(data) : new LanguageEditorModel();
this.isDeleted = data ? data.isActive === IsActive.Inactive : false;
this.buildForm();
}
} catch (error) { } catch (error) {
this.logger.error('Could not parse Language item: ' + data + error); this.logger.error('Could not parse Language item: ' + data + error);
this.uiNotificationService.snackBarNotification(this.language.instant('COMMONS.ERRORS.DEFAULT'), SnackBarNotificationLevel.Error); this.uiNotificationService.snackBarNotification(this.language.instant('COMMONS.ERRORS.DEFAULT'), SnackBarNotificationLevel.Error);

View File

@ -41,7 +41,7 @@ export class LanguageEditorModel extends BaseEditorModel implements LanguagePers
const baseValidationArray: Validation[] = new Array<Validation>(); const baseValidationArray: Validation[] = new Array<Validation>();
baseValidationArray.push({ key: 'id', validators: [BackendErrorValidator(this.validationErrorModel, 'id')] }); baseValidationArray.push({ key: 'id', validators: [BackendErrorValidator(this.validationErrorModel, 'id')] });
baseValidationArray.push({ key: 'code', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, 'code')] }); baseValidationArray.push({ key: 'code', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, 'code')] });
baseValidationArray.push({ key: 'payload', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, 'payload')] }); baseValidationArray.push({ key: 'payload', validators: [BackendErrorValidator(this.validationErrorModel, 'payload')] });
baseValidationArray.push({ key: 'hash', validators: [] }); baseValidationArray.push({ key: 'hash', validators: [] });
baseContext.validation = baseValidationArray; baseContext.validation = baseValidationArray;

View File

@ -1,30 +0,0 @@
<div *ngIf="parseFinished" class="language-editor" [style.height]="(keys.length * rowHeight) + 'px'">
<form (ngSubmit)="updateLang()" [formGroup]="formGroup">
<div class="row">
<div class="search-bar">
<input class="search-text" mat-input #search placeholder="{{ 'DASHBOARD.SEARCH' | translate }}">
<button mat-raised-button type="button" class="lightblue-btn search-btn" (click)="findKeys(search)">
<mat-icon>search</mat-icon>
</button>
</div>
<mat-card class="col-md-8 offset-md-2 sticky">
<mat-card-title><div [innerHTML]="currentLang"></div></mat-card-title>
<mat-card-content>
<div>
<div *ngFor="let key of visibleKeys">
<mat-form-field>
<mat-label>{{key}} :</mat-label>
<textarea matInput class="language-area" cdkTextareaAutosize #autosize="cdkTextareaAutosize" [formControl]="formGroup.get(key)">
</textarea>
</mat-form-field >
</div>
</div>
</mat-card-content>
</mat-card>
<button mat-fab class="mat-fab-bottom-right save-btn" type="submit">
<mat-icon class="mat-24" title="save">save</mat-icon>
</button>
</div>
</form>
</div>

View File

@ -1,51 +0,0 @@
.language-editor {
padding-top: 1em;
padding-bottom: 2em;
.language-area {
box-sizing: content-box;
}
.save-btn {
padding-top: inherit !important;
top: auto !important;
width: 56px !important;
bottom: 10px;
position: fixed;
right: 24px;
}
.sticky {
position: fixed;
left: 214px;
right: 214px;
width: 50%;
}
.search-bar {
padding-top: inherit !important;
bottom: auto !important;
width: 258px !important;
top: 100px;
position: fixed;
right: 24px;
background-color: white;
border: 1px solid rgb(218, 218, 218);
border-radius: 6px;
padding-left: 10px;
}
.search-text {
border: 0px solid rgb(218, 218, 218);
border-radius: 6px;
width: 180px;
}
.search-text::placeholder {
color: rgb(197, 194, 194);
line-height: 150%;
}
.search-btn {
left: 4px;
}
}

View File

@ -1,176 +0,0 @@
import { Component, OnInit, OnDestroy, ViewChild, AfterViewInit, ChangeDetectorRef, AfterViewChecked, ElementRef, ViewChildren, QueryList } from '@angular/core';
import { LanguageService } from '@app/core/services/language/language.service';
import { BaseComponent } from '@common/base/base.component';
import { takeUntil } from 'rxjs/operators';
import { UntypedFormGroup, UntypedFormBuilder } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
import { UiNotificationService, SnackBarNotificationLevel } from '@app/core/services/notification/ui-notification-service';
import { Router } from '@angular/router';
import { CdkTextareaAutosize } from '@angular/cdk/text-field';
import { MatomoService } from '@app/core/services/matomo/matomo-service';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-language-editor',
templateUrl: './language-editor.component.html',
styleUrls: ['./language-editor.component.scss']
})
export class LanguageEditorComponent extends BaseComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChildren('autosize', {read: CdkTextareaAutosize}) elements: QueryList<CdkTextareaAutosize>;
readonly rowHeight = 100;
readonly maxElements = 10;
allkeys = [];
keys = [];
visibleKeys = [];
startIndex = 0;
endIndex: number;
parseFinished = false;
currentLang: string;
formGroup: UntypedFormGroup;
formBuilder: UntypedFormBuilder;
constructor(
private language: LanguageService,
private uiNotificationService: UiNotificationService,
private translate: TranslateService,
private router: Router,
private httpClient: HttpClient,
private matomoService: MatomoService
) { super(); }
ngAfterViewInit(): void {
this.elements.changes.pipe(takeUntil(this._destroyed)).subscribe(items => {
if (this.elements.length > 0) {
this.elements.toArray().forEach(autosize => {
if (autosize instanceof CdkTextareaAutosize) {
this.setupAutosize(autosize);
} else {
console.log(autosize);
}
})
}
});
}
ngOnInit() {
this.matomoService.trackPageView('Admin: Language Editor');
this.formBuilder = new UntypedFormBuilder();
this.formGroup = this.formBuilder.group({});
this.endIndex = this.maxElements;
window.addEventListener('scroll', this.refreshFn, true);
this.language.getCurrentLanguageJSON()
.pipe(takeUntil(this._destroyed))
.subscribe(response => {
const blob = new Blob([response.body], { type: 'application/json' });
this.convertBlobToJSON(blob);
});
}
ngOnDestroy() {
window.removeEventListener('scroll', this.refreshFn, true);
}
convertBlobToJSON(blob: Blob) {
const fr = new FileReader();
fr.onload = ev => {
const langObj = JSON.parse(fr.result as string);
this.convertObjectToForm(langObj, '', this.formGroup);
this.currentLang = this.language.getCurrentLanguageName();
this.keys.length = 0;
for (const key of this.allkeys) {
this.keys.push(key);
}
this.visibleKeys = this.keys.slice(this.startIndex, this.endIndex);
this.parseFinished = true;
};
fr.readAsText(blob);
}
convertObjectToForm(obj: any, parentKey: string, form: UntypedFormGroup) {
for (let prop in obj) {
const key = parentKey !== '' ? `${parentKey}.${prop}` : prop;
if (typeof obj[prop] === 'object') {
form.addControl(prop, this.formBuilder.group({}));
this.convertObjectToForm(obj[prop], key, form.get(prop) as UntypedFormGroup);
continue;
} else {
form.addControl(prop, this.formBuilder.control(obj[prop]));
this.allkeys.push(key);
}
}
return;
}
updateLang() {
const json = JSON.stringify(this.formGroup.value, null, " ");
this.language.updateLanguage(json).pipe(takeUntil(this._destroyed))
.subscribe(
complete => {
this.onCallbackSuccess(complete);
},
error => {
this.onCallbackError(error);
}
);
}
onCallbackSuccess(id?: String): void {
this.uiNotificationService.snackBarNotification(this.translate.instant('GENERAL.SNACK-BAR.SUCCESSFUL-UPDATE'), SnackBarNotificationLevel.Success);
this.router.navigate(['/reload']).then(() => this.router.navigate(['/language-editor']));
}
onCallbackError(error: any) {
this.uiNotificationService.snackBarNotification(error, SnackBarNotificationLevel.Error);
//this.validateAllFormFields(this.formGroup);
}
refreshFn = (ev: Event) => {
const evDoc = (<HTMLBaseElement>ev.target);
let mainPage;
evDoc.childNodes.forEach(child => {
if ((<HTMLDivElement> child).id === 'main-page') {
mainPage = child;
}
});
if (document.scrollingElement !== undefined && mainPage !== undefined) {
this.startIndex = Math.floor(evDoc.scrollTop / this.rowHeight);
this.endIndex = this.startIndex + this.maxElements;
const tempKeys = this.keys.slice(this.startIndex, this.endIndex);
this.visibleKeys.length = 0;
for (const key of tempKeys) {
this.visibleKeys.push(key);
}
}
}
findKeys(ev: any) {
let tempKeys = [];
if (ev.value === "") {
tempKeys = this.allkeys;
} else {
tempKeys = this.allkeys.filter((key) => (this.formGroup.get(key).value as string).toLowerCase().includes(ev.value.toLowerCase()));
window.scrollTo({ top: 0 });
}
this.keys.length = 0;
for (const key of tempKeys) {
this.keys.push(key);
}
this.visibleKeys = this.keys.slice(this.startIndex, this.endIndex);
}
private setupAutosize(autosize: CdkTextareaAutosize) {
if (autosize !== undefined && autosize !== null) {
autosize.minRows = 1;
autosize.maxRows = 5;
autosize.enabled = true;
}
}
}

View File

@ -1,21 +0,0 @@
import { NgModule } from '@angular/core';
import { LanguageEditorComponent } from './language-editor.component';
import { LanguageEditorRoutingModule } from './language-editor.routing';
import { CommonUiModule } from '@common/ui/common-ui.module';
import { CommonFormsModule } from '@common/forms/common-forms.module';
import { ConfirmationDialogModule } from '@common/modules/confirmation-dialog/confirmation-dialog.module';
import {TextFieldModule} from '@angular/cdk/text-field';
@NgModule({
declarations: [LanguageEditorComponent],
imports: [
CommonUiModule,
CommonFormsModule,
ConfirmationDialogModule,
LanguageEditorRoutingModule,
TextFieldModule
]
})
export class LanguageEditorModule { }

View File

@ -1,16 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LanguageEditorComponent } from './language-editor.component';
import { AuthGuard } from '@app/core/auth-guard.service';
import { AdminAuthGuard } from '@app/core/admin-auth-guard.service';
const routes: Routes = [
{ path: '', component: LanguageEditorComponent, canActivate: [AdminAuthGuard] },
// { path: ':id', component: UserProfileComponent }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class LanguageEditorRoutingModule { }