From 0534cf0baecdcff453c5d816b922f12f014a0048 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Ha=C3=9Fel?= Date: Tue, 9 Nov 2021 12:43:39 +0100 Subject: [PATCH] ChannelList + Read/Write-Channel in PropertyList --- .../angular/src/app/api/ISearchService.ts | 6 +- src/main/angular/src/app/api/KeyValuePair.ts | 26 ------ src/main/angular/src/app/api/SearchResult.ts | 26 ++++++ .../angular/src/app/api/channel/Channel.ts | 85 ++++++++++++++++++ .../app/api/channel/channel.service.spec.ts | 16 ++++ .../src/app/api/channel/channel.service.ts | 39 +++++++++ .../angular/src/app/api/property/Property.ts | 9 +- .../src/app/api/property/property.service.ts | 10 +-- .../src/app/api/scene/scene.service.ts | 10 +-- .../angular/src/app/app-routing.module.ts | 3 + src/main/angular/src/app/app.component.html | 15 ++-- src/main/angular/src/app/app.component.less | 6 ++ src/main/angular/src/app/app.module.ts | 2 + .../channel-list/channel-list.component.html | 21 +++++ .../channel-list/channel-list.component.less | 3 + .../channel-list.component.spec.ts | 25 ++++++ .../channel-list/channel-list.component.ts | 43 +++++++++ .../device-list/device-list.component.less | 8 -- .../property-list.component.html | 87 +++++++++++-------- .../property-list/property-list.component.ts | 6 ++ .../edit-field/edit-field.component.html | 2 +- .../app/shared/search/search.component.html | 56 ++++++------ .../app/shared/search/search.component.less | 36 +++++--- .../src/app/shared/search/search.component.ts | 14 +-- src/main/angular/src/styles.less | 13 +++ .../ph87/homeautomation/channel/Channel.java | 2 + .../channel/ChannelController.java | 40 +++++++++ .../homeautomation/channel/ChannelDto.java | 24 +++++ .../channel/ChannelRepository.java | 7 ++ .../channel/ChannelService.java | 42 +++++---- .../homeautomation/channel/IChannelOwner.java | 8 ++ .../group/KnxGroupChannelOwnerService.java | 33 ++++--- .../homeautomation/knx/group/KnxGroupDto.java | 14 +-- .../knx/group/KnxGroupReadService.java | 10 +++ .../knx/group/KnxGroupRepository.java | 4 +- .../knx/group/KnxGroupWriteService.java | 3 +- .../property/PropertyChannelService.java | 36 ++++++++ .../property/PropertyController.java | 15 ++-- .../homeautomation/property/PropertyDto.java | 9 +- .../property/PropertyMapper.java | 5 +- .../shared/ISearchController.java | 4 +- .../homeautomation/shared/KeyValuePair.java | 17 ---- .../homeautomation/shared/SearchResult.java | 17 ++++ 43 files changed, 648 insertions(+), 209 deletions(-) delete mode 100644 src/main/angular/src/app/api/KeyValuePair.ts create mode 100644 src/main/angular/src/app/api/SearchResult.ts create mode 100644 src/main/angular/src/app/api/channel/Channel.ts create mode 100644 src/main/angular/src/app/api/channel/channel.service.spec.ts create mode 100644 src/main/angular/src/app/api/channel/channel.service.ts create mode 100644 src/main/angular/src/app/pages/channel-list/channel-list.component.html create mode 100644 src/main/angular/src/app/pages/channel-list/channel-list.component.less create mode 100644 src/main/angular/src/app/pages/channel-list/channel-list.component.spec.ts create mode 100644 src/main/angular/src/app/pages/channel-list/channel-list.component.ts create mode 100644 src/main/java/de/ph87/homeautomation/channel/ChannelController.java create mode 100644 src/main/java/de/ph87/homeautomation/channel/ChannelDto.java create mode 100644 src/main/java/de/ph87/homeautomation/channel/ChannelRepository.java create mode 100644 src/main/java/de/ph87/homeautomation/property/PropertyChannelService.java delete mode 100644 src/main/java/de/ph87/homeautomation/shared/KeyValuePair.java create mode 100644 src/main/java/de/ph87/homeautomation/shared/SearchResult.java diff --git a/src/main/angular/src/app/api/ISearchService.ts b/src/main/angular/src/app/api/ISearchService.ts index 8561f30..f6a98da 100644 --- a/src/main/angular/src/app/api/ISearchService.ts +++ b/src/main/angular/src/app/api/ISearchService.ts @@ -1,9 +1,9 @@ -import {KeyValuePair} from "./KeyValuePair"; +import {SearchResult} from "./SearchResult"; export interface ISearchService { - get(id: number, next: (results: KeyValuePair) => void, error: (error: any) => void): void; + get(id: number, next: (results: SearchResult) => void, error: (error: any) => void): void; - search(term: string, next: (results: KeyValuePair[]) => void, error: (error: any) => void): void; + search(term: string, next: (results: SearchResult[]) => void, error: (error: any) => void): void; } diff --git a/src/main/angular/src/app/api/KeyValuePair.ts b/src/main/angular/src/app/api/KeyValuePair.ts deleted file mode 100644 index 7783f3f..0000000 --- a/src/main/angular/src/app/api/KeyValuePair.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {validateStringNotEmptyNotNull} from "./validators"; - -export class KeyValuePair { - - constructor( - readonly key: string, - readonly value: string, - ) { - } - - static fromJson(json: any): KeyValuePair { - return new KeyValuePair( - validateStringNotEmptyNotNull(json['key']), - validateStringNotEmptyNotNull(json['value']), - ); - } - - public static trackBy(index: number, item: KeyValuePair): string { - return item.value; - } - - public static compareKey(a: KeyValuePair, b: KeyValuePair): number { - return a.value.localeCompare(b.value); - } - -} diff --git a/src/main/angular/src/app/api/SearchResult.ts b/src/main/angular/src/app/api/SearchResult.ts new file mode 100644 index 0000000..816ba67 --- /dev/null +++ b/src/main/angular/src/app/api/SearchResult.ts @@ -0,0 +1,26 @@ +import {validateNumberNotNull, validateStringNotEmptyNotNull} from "./validators"; + +export class SearchResult { + + constructor( + readonly id: number, + readonly title: string, + ) { + } + + static fromJson(json: any): SearchResult { + return new SearchResult( + validateNumberNotNull(json['id']), + validateStringNotEmptyNotNull(json['title']), + ); + } + + public static trackBy(index: number, item: SearchResult): string { + return item.title; + } + + public static compareTitle(a: SearchResult, b: SearchResult): number { + return a.title.localeCompare(b.title); + } + +} diff --git a/src/main/angular/src/app/api/channel/Channel.ts b/src/main/angular/src/app/api/channel/Channel.ts new file mode 100644 index 0000000..3bd1a1f --- /dev/null +++ b/src/main/angular/src/app/api/channel/Channel.ts @@ -0,0 +1,85 @@ +import {validateBooleanNotNull, validateDateAllowNull, validateNumberAllowNull, validateNumberNotNull, validateStringEmptyToNull, validateStringNotEmptyNotNull} from "../validators"; +import {prefix} from "../../helpers"; + +export abstract class Channel { + + constructor( + readonly id: number, + readonly title: string, + readonly type: string, + ) { + // nothing + } + + static fromJsonAllowNull(json: any): Channel | null { + if (!json) { + return null; + } + return this.fromJson(json); + } + + static fromJson(json: any): Channel { + const type: string = validateStringNotEmptyNotNull(json['type']); + switch (type) { + case "KnxGroup": + return new KnxGroup( + validateNumberNotNull(json['id']), + validateStringNotEmptyNotNull(json['title']), + type, + validateNumberNotNull(json['addressRaw']), + validateStringNotEmptyNotNull(json['addressStr']), + validateNumberNotNull(json['dptMain']), + validateNumberNotNull(json['dptSub']), + validateStringEmptyToNull(json['description']), + validateNumberNotNull(json['puid']), + validateBooleanNotNull(json['ets']), + validateNumberAllowNull(json['value']), + validateDateAllowNull(json['timestamp']), + ); + } + throw new Error("No such type: " + type); + } + + public static trackBy(index: number, item: Channel): number { + return item.id; + } + + public static compareTypeThenTitle(a: Channel, b: Channel): number { + const type: number = -a.type.localeCompare(b.type); + if (type !== 0) { + return type; + } + return a.title.localeCompare(b.title); + } +} + +export class KnxGroup extends Channel { + + public addresMain: number; + public addresMid: number; + public addresSub: number; + public dpt: string; + + constructor( + id: number, + title: string, + type: string, + public addressRaw: number, + public addressStr: string, + public dptMain: number, + public dptSub: number, + public description: string | null, + public puid: number, + public ets: boolean, + public value: number | null, + public timestamp: Date | null, + ) { + super(id, title, type); + const addressParts = this.addressStr.split("/"); + this.addresMain = parseInt(addressParts[0]); + this.addresMid = parseInt(addressParts[1]); + this.addresSub = parseInt(addressParts[2]); + this.dpt = this.dptMain + "." + prefix(this.dptSub, "0", 3); + } + +} diff --git a/src/main/angular/src/app/api/channel/channel.service.spec.ts b/src/main/angular/src/app/api/channel/channel.service.spec.ts new file mode 100644 index 0000000..9167918 --- /dev/null +++ b/src/main/angular/src/app/api/channel/channel.service.spec.ts @@ -0,0 +1,16 @@ +import {TestBed} from '@angular/core/testing'; + +import {ChannelService} from './channel.service'; + +describe('ChannelService', () => { + let service: ChannelService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ChannelService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/main/angular/src/app/api/channel/channel.service.ts b/src/main/angular/src/app/api/channel/channel.service.ts new file mode 100644 index 0000000..0ff2d20 --- /dev/null +++ b/src/main/angular/src/app/api/channel/channel.service.ts @@ -0,0 +1,39 @@ +import {Injectable} from '@angular/core'; +import {ApiService, NO_COMPARE, NO_OP} from "../api.service"; +import {ISearchService} from "../ISearchService"; +import {SearchResult} from "../SearchResult"; +import {Update} from "../Update"; +import {Channel} from "./Channel"; + +@Injectable({ + providedIn: 'root' +}) +export class ChannelService implements ISearchService { + + constructor( + readonly api: ApiService, + ) { + // nothing + } + + findAll(next: (list: Channel[]) => void, compare: (a: Channel, b: Channel) => number = NO_COMPARE, error: (error: any) => void = NO_OP): void { + this.api.getList("channel/findAll", Channel.fromJson, compare, next, error); + } + + subscribe(next: (channel: Update) => void): void { + this.api.subscribe("ChannelDto", Channel.fromJson, next); + } + + get(id: number, next: (results: SearchResult) => void, error: (error: any) => void): void { + this.api.getItem("channel/getById/" + id, SearchResult.fromJson, next, error); + } + + search(term: string, next: (results: SearchResult[]) => void = NO_OP, error: (error: any) => void = NO_OP): void { + this.api.postReturnList("channel/searchLike", term, SearchResult.fromJson, next, error); + } + + set(channel: Channel, key: string, value: any, next: (item: Channel) => void = NO_OP, error: (error: any) => void = NO_OP): void { + this.api.postReturnItem("channel/set/" + channel.id + "/" + key, value, Channel.fromJson, next, error); + } + +} diff --git a/src/main/angular/src/app/api/property/Property.ts b/src/main/angular/src/app/api/property/Property.ts index 00c3ec6..e8483c4 100644 --- a/src/main/angular/src/app/api/property/Property.ts +++ b/src/main/angular/src/app/api/property/Property.ts @@ -1,4 +1,5 @@ import {validateDateAllowNull, validateNumberAllowNull, validateNumberNotNull, validateStringNotEmptyNotNull} from "../validators"; +import {Channel} from "../channel/Channel"; export class Property { @@ -9,6 +10,8 @@ export class Property { public title: string, public value: number | null, public timestamp: Date | null, + public readChannel: Channel | null, + public writeChannel: Channel | null, ) { // nothing } @@ -28,11 +31,13 @@ export class Property { validateStringNotEmptyNotNull(json['title']), validateNumberAllowNull(json['value']), validateDateAllowNull(json['timestamp']), + Channel.fromJsonAllowNull(json['readChannel']), + Channel.fromJsonAllowNull(json['writeChannel']), ); } - public static trackBy(index: number, item: Property): string { - return item.name; + public static trackBy(index: number, item: Property): number { + return item.id; } public static compareTypeThenTitle(a: Property, b: Property): number { diff --git a/src/main/angular/src/app/api/property/property.service.ts b/src/main/angular/src/app/api/property/property.service.ts index d4994d1..ef9b5f6 100644 --- a/src/main/angular/src/app/api/property/property.service.ts +++ b/src/main/angular/src/app/api/property/property.service.ts @@ -1,7 +1,7 @@ import {Injectable} from '@angular/core'; import {ApiService, NO_COMPARE, NO_OP} from "../api.service"; import {ISearchService} from "../ISearchService"; -import {KeyValuePair} from "../KeyValuePair"; +import {SearchResult} from "../SearchResult"; import {Update} from "../Update"; import {Property} from "./Property"; @@ -24,12 +24,12 @@ export class PropertyService implements ISearchService { this.api.subscribe("PropertyDto", Property.fromJson, next); } - get(id: number, next: (results: KeyValuePair) => void, error: (error: any) => void): void { - this.api.getItem("property/getById/" + id, KeyValuePair.fromJson, next, error); + get(id: number, next: (results: SearchResult) => void, error: (error: any) => void): void { + this.api.getItem("property/getById/" + id, SearchResult.fromJson, next, error); } - search(term: string, next: (results: KeyValuePair[]) => void = NO_OP, error: (error: any) => void = NO_OP): void { - this.api.postReturnList("property/searchLike", term, KeyValuePair.fromJson, next, error); + search(term: string, next: (results: SearchResult[]) => void = NO_OP, error: (error: any) => void = NO_OP): void { + this.api.postReturnList("property/searchLike", term, SearchResult.fromJson, next, error); } set(property: Property, key: string, value: any, next: (item: Property) => void = NO_OP, error: (error: any) => void = NO_OP): void { diff --git a/src/main/angular/src/app/api/scene/scene.service.ts b/src/main/angular/src/app/api/scene/scene.service.ts index 87bc51c..6f7a60c 100644 --- a/src/main/angular/src/app/api/scene/scene.service.ts +++ b/src/main/angular/src/app/api/scene/scene.service.ts @@ -1,7 +1,7 @@ import {Injectable} from '@angular/core'; import {ApiService, NO_COMPARE, NO_OP} from "../api.service"; import {ISearchService} from "../ISearchService"; -import {KeyValuePair} from "../KeyValuePair"; +import {SearchResult} from "../SearchResult"; import {Update} from "../Update"; import {Scene} from "./Scene"; @@ -24,12 +24,12 @@ export class SceneService implements ISearchService { this.api.subscribe("SceneDto", Scene.fromJson, next); } - get(id: number, next: (results: KeyValuePair) => void, error: (error: any) => void): void { - this.api.getItem("scene/getById/" + id, KeyValuePair.fromJson, next, error); + get(id: number, next: (results: SearchResult) => void, error: (error: any) => void): void { + this.api.getItem("scene/getById/" + id, SearchResult.fromJson, next, error); } - search(term: string, next: (results: KeyValuePair[]) => void = NO_OP, error: (error: any) => void = NO_OP): void { - this.api.postReturnList("scene/searchLike", term, KeyValuePair.fromJson, next, error); + search(term: string, next: (results: SearchResult[]) => void = NO_OP, error: (error: any) => void = NO_OP): void { + this.api.postReturnList("scene/searchLike", term, SearchResult.fromJson, next, error); } set(scene: Scene, key: string, value: any, next: (item: Scene) => void = NO_OP, error: (error: any) => void = NO_OP): void { diff --git a/src/main/angular/src/app/app-routing.module.ts b/src/main/angular/src/app/app-routing.module.ts index e17e770..6befd68 100644 --- a/src/main/angular/src/app/app-routing.module.ts +++ b/src/main/angular/src/app/app-routing.module.ts @@ -5,10 +5,13 @@ import {ScheduleComponent} from "./pages/schedule/schedule.component"; import {DeviceListComponent} from "./pages/device-list/device-list.component"; import {DeviceComponent} from "./pages/device/device.component"; import {PropertyListComponent} from "./pages/property-list/property-list.component"; +import {ChannelListComponent} from "./pages/channel-list/channel-list.component"; const routes: Routes = [ {path: 'Device', component: DeviceComponent}, {path: 'DeviceList', component: DeviceListComponent}, + // {path: 'Channel', component: ChannelComponent}, + {path: 'ChannelList', component: ChannelListComponent}, // {path: 'Property', component: PropertyComponent}, {path: 'PropertyList', component: PropertyListComponent}, {path: 'Schedule', component: ScheduleComponent}, diff --git a/src/main/angular/src/app/app.component.html b/src/main/angular/src/app/app.component.html index 3c06b28..649ffc7 100644 --- a/src/main/angular/src/app/app.component.html +++ b/src/main/angular/src/app/app.component.html @@ -3,18 +3,17 @@
Zeitpläne
- -
- Eigenschaften -
-
Geräte
- - - +
+ Eigenschaften +
+ +
+ Kanäle +
diff --git a/src/main/angular/src/app/app.component.less b/src/main/angular/src/app/app.component.less index 825e53a..d18eb6b 100644 --- a/src/main/angular/src/app/app.component.less +++ b/src/main/angular/src/app/app.component.less @@ -7,6 +7,12 @@ border-right: 1px solid black; } + .itemSecondary { + float: right; + border-left: 1px solid black; + border-right: none; + } + .item:hover { background-color: lightskyblue; } diff --git a/src/main/angular/src/app/app.module.ts b/src/main/angular/src/app/app.module.ts index 77ffa18..7539e54 100644 --- a/src/main/angular/src/app/app.module.ts +++ b/src/main/angular/src/app/app.module.ts @@ -14,6 +14,7 @@ import {SearchComponent} from './shared/search/search.component'; import {DeviceListComponent} from './pages/device-list/device-list.component'; import {DeviceComponent} from './pages/device/device.component'; import {PropertyListComponent} from './pages/property-list/property-list.component'; +import {ChannelListComponent} from './pages/channel-list/channel-list.component'; @NgModule({ declarations: [ @@ -26,6 +27,7 @@ import {PropertyListComponent} from './pages/property-list/property-list.compone DeviceListComponent, DeviceComponent, PropertyListComponent, + ChannelListComponent, ], imports: [ BrowserModule, diff --git a/src/main/angular/src/app/pages/channel-list/channel-list.component.html b/src/main/angular/src/app/pages/channel-list/channel-list.component.html new file mode 100644 index 0000000..5d58b3c --- /dev/null +++ b/src/main/angular/src/app/pages/channel-list/channel-list.component.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + +
TitelTypAdresseDPTWert
{{channel.title}}{{channel.type}}{{asKnxGroup(channel).addresMain}} / {{asKnxGroup(channel).addresMid}} / {{asKnxGroup(channel).addresSub}}{{asKnxGroup(channel).dpt}}{{asKnxGroup(channel).value}}{{asKnxGroup(channel).timestamp | date:'yyyy-MM-dd HH:mm:ss'}}
diff --git a/src/main/angular/src/app/pages/channel-list/channel-list.component.less b/src/main/angular/src/app/pages/channel-list/channel-list.component.less new file mode 100644 index 0000000..1922e7f --- /dev/null +++ b/src/main/angular/src/app/pages/channel-list/channel-list.component.less @@ -0,0 +1,3 @@ +table { + width: 100%; +} diff --git a/src/main/angular/src/app/pages/channel-list/channel-list.component.spec.ts b/src/main/angular/src/app/pages/channel-list/channel-list.component.spec.ts new file mode 100644 index 0000000..6adc500 --- /dev/null +++ b/src/main/angular/src/app/pages/channel-list/channel-list.component.spec.ts @@ -0,0 +1,25 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {ChannelListComponent} from './channel-list.component'; + +describe('ChannelListComponent', () => { + let component: ChannelListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ChannelListComponent] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ChannelListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/main/angular/src/app/pages/channel-list/channel-list.component.ts b/src/main/angular/src/app/pages/channel-list/channel-list.component.ts new file mode 100644 index 0000000..71f0bc7 --- /dev/null +++ b/src/main/angular/src/app/pages/channel-list/channel-list.component.ts @@ -0,0 +1,43 @@ +import {Component, OnInit} from '@angular/core'; +import {ChannelService} from "../../api/channel/channel.service"; +import {Channel, KnxGroup} from "../../api/channel/Channel"; +import {Update} from "../../api/Update"; + +@Component({ + selector: 'app-channel-list', + templateUrl: './channel-list.component.html', + styleUrls: ['./channel-list.component.less'] +}) +export class ChannelListComponent implements OnInit { + + channels: Channel[] = []; + + constructor( + readonly channelService: ChannelService, + ) { + // nothing + } + + ngOnInit(): void { + this.channelService.subscribe(update => this.updateChannel(update)) + this.channelService.findAll(channels => this.channels = channels); + } + + asKnxGroup(channel: Channel): KnxGroup { + return channel as KnxGroup; + } + + private updateChannel(update: Update): void { + const index: number = this.channels.findIndex(c => c.id === update.payload.id); + if (index >= 0) { + if (update.existing) { + this.channels[index] = update.payload; + } else { + this.channels.slice(index, 1); + } + } else { + this.channels.push(update.payload); + } + } + +} diff --git a/src/main/angular/src/app/pages/device-list/device-list.component.less b/src/main/angular/src/app/pages/device-list/device-list.component.less index 123bc42..299ef49 100644 --- a/src/main/angular/src/app/pages/device-list/device-list.component.less +++ b/src/main/angular/src/app/pages/device-list/device-list.component.less @@ -29,14 +29,6 @@ padding: 5px; margin: 5px; border-radius: 25%; - - .center { - position: absolute; - margin: 0; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - } } .button { diff --git a/src/main/angular/src/app/pages/property-list/property-list.component.html b/src/main/angular/src/app/pages/property-list/property-list.component.html index 7ea6791..add2705 100644 --- a/src/main/angular/src/app/pages/property-list/property-list.component.html +++ b/src/main/angular/src/app/pages/property-list/property-list.component.html @@ -1,3 +1,7 @@ + + - + + @@ -5,49 +9,56 @@ + + - - - - - - + - - - + + + + + + + - - - - - - - - + +
BezeichnungTyp Wert ZeitstempelLesekanalSchreibkanal
- - - - - - - {{property.value ? "An" : "Aus"}} + +
+ - {{property.value}} % + + - {{property.value}} % + + - {{property.value}} K + + + {{property.value ? "An" : "Aus"}} + + {{property.value}} % + + {{property.value}} % + + {{property.value}} K + + {{property.value | number:'0.0-0'}} lux + + {{findScene(property)?.title || "Unbekannt: " + property.value}} + + {{property.timestamp | date:'yyyy-MM-dd HH:mm:ss'}} - {{property.value | number:'0.0-0'}} lux + + - {{findScene(property)?.title || "Unbekannt: " + property.value}} + + - -LEER- - {{property.timestamp | date:'yyyy-MM-dd HH:mm:ss'}}
diff --git a/src/main/angular/src/app/pages/property-list/property-list.component.ts b/src/main/angular/src/app/pages/property-list/property-list.component.ts index 7e3ff4b..c495d77 100644 --- a/src/main/angular/src/app/pages/property-list/property-list.component.ts +++ b/src/main/angular/src/app/pages/property-list/property-list.component.ts @@ -3,6 +3,7 @@ import {Property} from "../../api/property/Property"; import {PropertyService} from "../../api/property/property.service"; import {Scene} from "../../api/scene/Scene"; import {SceneService} from "../../api/scene/scene.service"; +import {ChannelService} from "../../api/channel/channel.service"; @Component({ selector: 'app-property-list', @@ -20,6 +21,7 @@ export class PropertyListComponent implements OnInit { constructor( readonly propertyService: PropertyService, readonly sceneService: SceneService, + readonly channelService: ChannelService, ) { // nothing } @@ -70,4 +72,8 @@ export class PropertyListComponent implements OnInit { return this.scenes.find(s => s.id === property.value); } + set(property: Property, key: string, value: any): void { + + } + } diff --git a/src/main/angular/src/app/shared/edit-field/edit-field.component.html b/src/main/angular/src/app/shared/edit-field/edit-field.component.html index f1c22ca..38b4686 100644 --- a/src/main/angular/src/app/shared/edit-field/edit-field.component.html +++ b/src/main/angular/src/app/shared/edit-field/edit-field.component.html @@ -1,5 +1,5 @@
{{initial}} - - LEER - + -
diff --git a/src/main/angular/src/app/shared/search/search.component.html b/src/main/angular/src/app/shared/search/search.component.html index b0d8db4..35eddda 100644 --- a/src/main/angular/src/app/shared/search/search.component.html +++ b/src/main/angular/src/app/shared/search/search.component.html @@ -1,31 +1,33 @@ -
- - {{selected.value}} - - [{{selected.key}}] - - - - -LEER- - -
+
- +
+ + {{selected.title}} + + [{{selected.id}}] + + + - +
-
-
- - -
-
- {{selected.value}} - - [{{selected.key}}] - -
-
- {{result.value}} - - [{{result.key}}] - + + +
+
+ - +
+
+ {{selected.title}} + + [{{selected.id}}] + +
+
+ {{result.title}} + + [{{result.id}}] + +
+
diff --git a/src/main/angular/src/app/shared/search/search.component.less b/src/main/angular/src/app/shared/search/search.component.less index 2bc9ee1..30e4f3a 100644 --- a/src/main/angular/src/app/shared/search/search.component.less +++ b/src/main/angular/src/app/shared/search/search.component.less @@ -1,19 +1,27 @@ -.selected { - font-weight: bold; - border-bottom: 1px solid black; -} - -.resultList { - position: absolute; - background-color: lightgray; - min-width: 200px; - border: 1px solid black; - - .result { +.all { + .initial { padding: 5px; + height: 100%; } - .result:hover { - background-color: lightyellow; + .selected { + font-weight: bold; + border-bottom: 1px solid black; } + + .resultList { + position: absolute; + background-color: lightgray; + min-width: 200px; + border: 1px solid black; + + .result { + padding: 5px; + } + + .result:hover { + background-color: lightyellow; + } + } + } diff --git a/src/main/angular/src/app/shared/search/search.component.ts b/src/main/angular/src/app/shared/search/search.component.ts index ffb05b1..3fc7f0c 100644 --- a/src/main/angular/src/app/shared/search/search.component.ts +++ b/src/main/angular/src/app/shared/search/search.component.ts @@ -1,5 +1,5 @@ import {Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core'; -import {KeyValuePair} from "../../api/KeyValuePair"; +import {SearchResult} from "../../api/SearchResult"; import {ISearchService} from "../../api/ISearchService"; @Component({ @@ -33,13 +33,13 @@ export class SearchComponent implements OnInit { allowEmpty: boolean = true; @Output() - valueChange: EventEmitter = new EventEmitter(); + valueChange: EventEmitter = new EventEmitter(); term: string = ""; - results: KeyValuePair[] = []; + results: SearchResult[] = []; - selected?: KeyValuePair; + selected?: SearchResult; searching: boolean = false; @@ -67,7 +67,7 @@ export class SearchComponent implements OnInit { } start(): void { - this.term = this.selected?.value || ""; + this.term = this.selected?.title || ""; if (this.resultList && this.input) { this.resultList.style.left = this.input.style.left; } @@ -110,10 +110,10 @@ export class SearchComponent implements OnInit { this.cancelOnBlur = false; } - select(result: KeyValuePair | undefined): void { + select(result: SearchResult | undefined): void { this.searching = false; this.selected = result; - this.valueChange.emit(this.selected?.key); + this.valueChange.emit(this.selected?.id); } } diff --git a/src/main/angular/src/styles.less b/src/main/angular/src/styles.less index 8822d5d..307568b 100644 --- a/src/main/angular/src/styles.less +++ b/src/main/angular/src/styles.less @@ -19,7 +19,12 @@ img { table { border-collapse: collapse; + th { + background-color: gray; + } + td, th { + height: 0; // (=> auto growth) enables use of height percent for children padding: 5px; border: 1px solid black; @@ -36,6 +41,14 @@ table.vertical { } } +.center { + position: absolute; + margin: 0; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + .empty { text-align: center; color: gray; diff --git a/src/main/java/de/ph87/homeautomation/channel/Channel.java b/src/main/java/de/ph87/homeautomation/channel/Channel.java index cb372e3..f18fc3f 100644 --- a/src/main/java/de/ph87/homeautomation/channel/Channel.java +++ b/src/main/java/de/ph87/homeautomation/channel/Channel.java @@ -18,6 +18,8 @@ public abstract class Channel { @Setter(AccessLevel.NONE) private Long id; + public abstract String getName(); + public abstract Class getChannelOwnerClass(); public abstract Double getValue(); diff --git a/src/main/java/de/ph87/homeautomation/channel/ChannelController.java b/src/main/java/de/ph87/homeautomation/channel/ChannelController.java new file mode 100644 index 0000000..69c3a3e --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/channel/ChannelController.java @@ -0,0 +1,40 @@ +package de.ph87.homeautomation.channel; + +import de.ph87.homeautomation.shared.ISearchController; +import de.ph87.homeautomation.shared.SearchResult; +import de.ph87.homeautomation.web.NotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("channel") +@RequiredArgsConstructor +public class ChannelController implements ISearchController { + + private final ChannelService channelService; + + @GetMapping("findAll") + public List findAll() { + return channelService.findAllDto(); + } + + @Override + @GetMapping("getById/{id}") + public SearchResult getById(@PathVariable final long id) { + return channelService.findDtoById(id).map(this::toSearchResult).orElseThrow(() -> new NotFoundException("Channel.id=" + id)); + } + + @Override + @PostMapping("searchLike") + public List searchLike(@RequestBody final String term) { + return channelService.findAllDtoLike(term).stream().map(this::toSearchResult).collect(Collectors.toList()); + } + + private SearchResult toSearchResult(final ChannelDto dto) { + return new SearchResult(dto.getId(), dto.getTitle()); + } + +} diff --git a/src/main/java/de/ph87/homeautomation/channel/ChannelDto.java b/src/main/java/de/ph87/homeautomation/channel/ChannelDto.java new file mode 100644 index 0000000..30dd9af --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/channel/ChannelDto.java @@ -0,0 +1,24 @@ +package de.ph87.homeautomation.channel; + +import lombok.Getter; +import lombok.ToString; + +import java.io.Serializable; + +@Getter +@ToString +public abstract class ChannelDto implements Serializable { + + private final long id; + + private final String title; + + private final String type; + + protected ChannelDto(final Channel channel) { + this.id = channel.getId(); + this.title = channel.getName(); + this.type = channel.getClass().getSimpleName(); + } + +} diff --git a/src/main/java/de/ph87/homeautomation/channel/ChannelRepository.java b/src/main/java/de/ph87/homeautomation/channel/ChannelRepository.java new file mode 100644 index 0000000..8049107 --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/channel/ChannelRepository.java @@ -0,0 +1,7 @@ +package de.ph87.homeautomation.channel; + +import org.springframework.data.repository.CrudRepository; + +public interface ChannelRepository extends CrudRepository { + +} diff --git a/src/main/java/de/ph87/homeautomation/channel/ChannelService.java b/src/main/java/de/ph87/homeautomation/channel/ChannelService.java index a3dbeb9..fb4dba5 100644 --- a/src/main/java/de/ph87/homeautomation/channel/ChannelService.java +++ b/src/main/java/de/ph87/homeautomation/channel/ChannelService.java @@ -1,14 +1,13 @@ package de.ph87.homeautomation.channel; import de.ph87.homeautomation.property.Property; -import de.ph87.homeautomation.property.PropertyReadService; +import de.ph87.homeautomation.shared.Helpers; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.context.event.ApplicationStartedEvent; -import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -20,19 +19,7 @@ public class ChannelService { private final List channelOwners; - private final PropertyReadService propertyReadService; - - @EventListener(ApplicationStartedEvent.class) - public void readAllPropertyChannels() { - propertyReadService.findAllByReadChannelNotNull().forEach(property -> { - final Optional ownerOptional = findByChannel(property.getReadChannel()); - if (ownerOptional.isPresent()) { - ownerOptional.get().read(property.getReadChannel()); - } else { - log.error("No Owner for Property: {}", property); - } - }); - } + private final ChannelRepository channelRepository; public Optional findByChannel(final Channel channel) { return channelOwners.stream().filter(owner -> channel.getChannelOwnerClass().isInstance(owner)).findFirst(); @@ -50,4 +37,27 @@ public class ChannelService { getByChannel(channel).write(property.getWriteChannel(), value); } + public ChannelDto toDtoAllowNull(final Channel channel) { + if (channel == null) { + return null; + } + return toDto(channel); + } + + public ChannelDto toDto(final Channel channel) { + return getByChannel(channel).toDto(channel); + } + + public List findAllDto() { + return channelOwners.stream().map(IChannelOwner::findAllDto).reduce(new ArrayList<>(), Helpers::merge); + } + + public List findAllDtoLike(final String term) { + return channelOwners.stream().map(owner -> owner.findAllDtoLike(term)).reduce(new ArrayList<>(), Helpers::merge); + } + + public Optional findDtoById(final long id) { + return channelRepository.findById(id).map(this::toDto); + } + } diff --git a/src/main/java/de/ph87/homeautomation/channel/IChannelOwner.java b/src/main/java/de/ph87/homeautomation/channel/IChannelOwner.java index 7c5a8db..10345ff 100644 --- a/src/main/java/de/ph87/homeautomation/channel/IChannelOwner.java +++ b/src/main/java/de/ph87/homeautomation/channel/IChannelOwner.java @@ -1,9 +1,17 @@ package de.ph87.homeautomation.channel; +import java.util.List; + public interface IChannelOwner { void read(final Channel channel); void write(final Channel channel, final double value); + ChannelDto toDto(final Channel channel); + + List findAllDto(); + + List findAllDtoLike(final String like); + } diff --git a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupChannelOwnerService.java b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupChannelOwnerService.java index b23cc8d..d8a4f30 100644 --- a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupChannelOwnerService.java +++ b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupChannelOwnerService.java @@ -1,12 +1,16 @@ package de.ph87.homeautomation.knx.group; import de.ph87.homeautomation.channel.Channel; +import de.ph87.homeautomation.channel.ChannelDto; import de.ph87.homeautomation.channel.IChannelOwner; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; +import java.util.stream.Collectors; + @Slf4j @Service @Transactional @@ -15,22 +19,31 @@ public class KnxGroupChannelOwnerService implements IChannelOwner { private final KnxGroupWriteService knxGroupWriteService; + private final KnxGroupReadService knxGroupReadService; + @Override public void read(final Channel channel) { - if (!(channel instanceof KnxGroup)) { - throw new RuntimeException(); - } - final KnxGroup knxGroup = (KnxGroup) channel; - knxGroupWriteService.requestRead(knxGroup); + knxGroupWriteService.requestRead((KnxGroup) channel); } @Override public void write(final Channel channel, final double value) { - if (!(channel instanceof KnxGroup)) { - throw new RuntimeException(); - } - final KnxGroup knxGroup = (KnxGroup) channel; - knxGroupWriteService.requestWrite(knxGroup, value); + knxGroupWriteService.requestWrite((KnxGroup) channel, value); + } + + @Override + public ChannelDto toDto(final Channel channel) { + return new KnxGroupDto((KnxGroup) channel); + } + + @Override + public List findAllDto() { + return knxGroupReadService.findAll().stream().map(this::toDto).collect(Collectors.toList()); + } + + @Override + public List findAllDtoLike(final String like) { + return knxGroupReadService.findAllLike(like).stream().map(this::toDto).collect(Collectors.toList()); } } diff --git a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupDto.java b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupDto.java index d27cedb..6948146 100644 --- a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupDto.java +++ b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupDto.java @@ -1,12 +1,14 @@ package de.ph87.homeautomation.knx.group; -import lombok.Data; +import de.ph87.homeautomation.channel.ChannelDto; +import lombok.Getter; +import lombok.ToString; -import java.io.Serializable; import java.time.ZonedDateTime; -@Data -public class KnxGroupDto implements Serializable { +@Getter +@ToString(callSuper = true) +public class KnxGroupDto extends ChannelDto { public final int addressRaw; @@ -16,8 +18,6 @@ public class KnxGroupDto implements Serializable { public final int dptSub; - public final String name; - public final String description; public final int puid; @@ -37,11 +37,11 @@ public class KnxGroupDto implements Serializable { public final KnxGroupLinkInfo send; public KnxGroupDto(final KnxGroup knxGroup) { + super(knxGroup); this.addressRaw = knxGroup.getAddressRaw(); this.addressStr = knxGroup.getAddressStr(); this.dptMain = knxGroup.getDptMain(); this.dptSub = knxGroup.getDptSub(); - this.name = knxGroup.getName(); this.description = knxGroup.getDescription(); this.puid = knxGroup.getPuid(); this.ets = knxGroup.isEts(); diff --git a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupReadService.java b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupReadService.java index 2402636..7922bd1 100644 --- a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupReadService.java +++ b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupReadService.java @@ -6,6 +6,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import tuwien.auto.calimero.GroupAddress; +import java.util.List; + @Slf4j @Service @Transactional @@ -18,4 +20,12 @@ public class KnxGroupReadService { return knxGroupRepository.findByAddressRaw(new GroupAddress(main, mid, sub).getRawAddress()).orElseThrow(RuntimeException::new); } + public List findAll() { + return knxGroupRepository.findAll(); + } + + public List findAllLike(final String term) { + return knxGroupRepository.findAllByNameContainsIgnoreCaseOrAddressStrContainsIgnoreCase(term, term); + } + } diff --git a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupRepository.java b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupRepository.java index 654045a..dd67f7b 100644 --- a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupRepository.java +++ b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupRepository.java @@ -6,7 +6,7 @@ import java.time.ZonedDateTime; import java.util.List; import java.util.Optional; -public interface KnxGroupRepository extends CrudRepository { +public interface KnxGroupRepository extends CrudRepository { Optional findByAddressRaw(int rawAddress); @@ -18,6 +18,6 @@ public interface KnxGroupRepository extends CrudRepository { Optional findFirstByRead_NextTimestampNotNullOrderByRead_NextTimestampAsc(); - boolean existsByAddressRaw(int rawAddress); + List findAllByNameContainsIgnoreCaseOrAddressStrContainsIgnoreCase(String name, String addressStr); } diff --git a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupWriteService.java b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupWriteService.java index 515861a..f711f05 100644 --- a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupWriteService.java +++ b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupWriteService.java @@ -1,6 +1,7 @@ package de.ph87.homeautomation.knx.group; import de.ph87.homeautomation.channel.ChannelChangedEvent; +import de.ph87.homeautomation.channel.ChannelDto; import de.ph87.homeautomation.web.WebSocketService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -120,7 +121,7 @@ public class KnxGroupWriteService { private KnxGroupDto publish(final KnxGroup knxGroup) { final KnxGroupDto dto = knxGroupMapper.toDto(knxGroup); - webSocketService.send(dto, true); + webSocketService.send(ChannelDto.class.getSimpleName(), dto, true); return dto; } diff --git a/src/main/java/de/ph87/homeautomation/property/PropertyChannelService.java b/src/main/java/de/ph87/homeautomation/property/PropertyChannelService.java new file mode 100644 index 0000000..d1bf810 --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/property/PropertyChannelService.java @@ -0,0 +1,36 @@ +package de.ph87.homeautomation.property; + +import de.ph87.homeautomation.channel.ChannelService; +import de.ph87.homeautomation.channel.IChannelOwner; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Slf4j +@Service +@Transactional +@RequiredArgsConstructor +public class PropertyChannelService { + + private final ChannelService channelService; + + private final PropertyReadService propertyReadService; + + @EventListener(ApplicationStartedEvent.class) + public void readAllPropertyChannels() { + propertyReadService.findAllByReadChannelNotNull().forEach(property -> { + final Optional ownerOptional = channelService.findByChannel(property.getReadChannel()); + if (ownerOptional.isPresent()) { + ownerOptional.get().read(property.getReadChannel()); + } else { + log.error("No Owner for Property: {}", property); + } + }); + } + +} diff --git a/src/main/java/de/ph87/homeautomation/property/PropertyController.java b/src/main/java/de/ph87/homeautomation/property/PropertyController.java index cd2b586..102c68b 100644 --- a/src/main/java/de/ph87/homeautomation/property/PropertyController.java +++ b/src/main/java/de/ph87/homeautomation/property/PropertyController.java @@ -1,7 +1,7 @@ package de.ph87.homeautomation.property; import de.ph87.homeautomation.shared.ISearchController; -import de.ph87.homeautomation.shared.KeyValuePair; +import de.ph87.homeautomation.shared.SearchResult; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -44,19 +44,18 @@ public class PropertyController implements ISearchController { @Override @GetMapping("getById/{id}") - public KeyValuePair getById(@PathVariable final long id) { - final PropertyDto propertyDto = propertyReadService.getDtoById(id); - return toKeyValuePair(propertyDto); + public SearchResult getById(@PathVariable final long id) { + return toSearchResult(propertyReadService.getDtoById(id)); } @Override @PostMapping("searchLike") - public List searchLike(@RequestBody final String term) { - return propertyReadService.findAllDtoLike("%" + term + "%").stream().map(this::toKeyValuePair).collect(Collectors.toList()); + public List searchLike(@RequestBody final String term) { + return propertyReadService.findAllDtoLike("%" + term + "%").stream().map(this::toSearchResult).collect(Collectors.toList()); } - private KeyValuePair toKeyValuePair(final PropertyDto propertyDto) { - return new KeyValuePair(propertyDto.getName(), propertyDto.getTitle()); + private SearchResult toSearchResult(final PropertyDto propertyDto) { + return new SearchResult(propertyDto.getId(), propertyDto.getTitle()); } } diff --git a/src/main/java/de/ph87/homeautomation/property/PropertyDto.java b/src/main/java/de/ph87/homeautomation/property/PropertyDto.java index 445596a..76fce3f 100644 --- a/src/main/java/de/ph87/homeautomation/property/PropertyDto.java +++ b/src/main/java/de/ph87/homeautomation/property/PropertyDto.java @@ -1,5 +1,6 @@ package de.ph87.homeautomation.property; +import de.ph87.homeautomation.channel.ChannelDto; import lombok.Data; import java.io.Serializable; @@ -20,13 +21,19 @@ public final class PropertyDto implements Serializable { private final ZonedDateTime timestamp; - public PropertyDto(final Property property) { + private final ChannelDto readChannel; + + private final ChannelDto writeChannel; + + public PropertyDto(final Property property, final ChannelDto readChannel, final ChannelDto writeChannel) { this.id = property.getId(); this.type = property.getType(); this.name = property.getName(); this.title = property.getTitle(); this.value = property.getValue(); this.timestamp = property.getTimestamp(); + this.readChannel = readChannel; + this.writeChannel = writeChannel; } } diff --git a/src/main/java/de/ph87/homeautomation/property/PropertyMapper.java b/src/main/java/de/ph87/homeautomation/property/PropertyMapper.java index 9e2f908..f11b896 100644 --- a/src/main/java/de/ph87/homeautomation/property/PropertyMapper.java +++ b/src/main/java/de/ph87/homeautomation/property/PropertyMapper.java @@ -1,5 +1,6 @@ package de.ph87.homeautomation.property; +import de.ph87.homeautomation.channel.ChannelService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -11,8 +12,10 @@ import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor public class PropertyMapper { + private final ChannelService channelService; + public PropertyDto toDto(final Property property) { - return new PropertyDto(property); + return new PropertyDto(property, channelService.toDtoAllowNull(property.getReadChannel()), channelService.toDtoAllowNull(property.getWriteChannel())); } } diff --git a/src/main/java/de/ph87/homeautomation/shared/ISearchController.java b/src/main/java/de/ph87/homeautomation/shared/ISearchController.java index 1dda3f4..890cd14 100644 --- a/src/main/java/de/ph87/homeautomation/shared/ISearchController.java +++ b/src/main/java/de/ph87/homeautomation/shared/ISearchController.java @@ -4,8 +4,8 @@ import java.util.List; public interface ISearchController { - KeyValuePair getById(final long id); + SearchResult getById(final long id); - List searchLike(final String term); + List searchLike(final String term); } diff --git a/src/main/java/de/ph87/homeautomation/shared/KeyValuePair.java b/src/main/java/de/ph87/homeautomation/shared/KeyValuePair.java deleted file mode 100644 index e54268c..0000000 --- a/src/main/java/de/ph87/homeautomation/shared/KeyValuePair.java +++ /dev/null @@ -1,17 +0,0 @@ -package de.ph87.homeautomation.shared; - -import lombok.Getter; - -@Getter -public class KeyValuePair { - - public final String key; - - public final String value; - - public KeyValuePair(final String key, final String value) { - this.key = key; - this.value = value; - } - -} diff --git a/src/main/java/de/ph87/homeautomation/shared/SearchResult.java b/src/main/java/de/ph87/homeautomation/shared/SearchResult.java new file mode 100644 index 0000000..c1ada88 --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/shared/SearchResult.java @@ -0,0 +1,17 @@ +package de.ph87.homeautomation.shared; + +import lombok.Getter; + +@Getter +public class SearchResult { + + public final long id; + + public final String title; + + public SearchResult(final long id, final String title) { + this.id = id; + this.title = title; + } + +}