2020-06-02 16:47:46 +02:00
|
|
|
import 'dart:math';
|
|
|
|
|
|
|
|
|
|
import 'package:moor/moor.dart';
|
|
|
|
|
import 'package:musicus_client/musicus_client.dart';
|
|
|
|
|
|
|
|
|
|
import 'info.dart';
|
|
|
|
|
|
|
|
|
|
part 'database.g.dart';
|
|
|
|
|
|
|
|
|
|
final _random = Random(DateTime.now().millisecondsSinceEpoch);
|
|
|
|
|
|
|
|
|
|
/// Generate a random ID suitable for use as primary key.
|
|
|
|
|
int generateId() => _random.nextInt(0xFFFFFFFF);
|
|
|
|
|
|
|
|
|
|
/// The database for storing all metadata for the music library.
|
|
|
|
|
///
|
|
|
|
|
/// This also handles synchronization with a Musicus server.
|
|
|
|
|
@UseMoor(include: {'database.moor'})
|
|
|
|
|
class MusicusClientDatabase extends _$MusicusClientDatabase {
|
|
|
|
|
/// The number of items contained in one result page.
|
|
|
|
|
static const pageSize = 50;
|
|
|
|
|
|
|
|
|
|
/// The client to use for synchronization.
|
|
|
|
|
///
|
|
|
|
|
/// This may be null indicating that everything should be kept local.
|
2020-06-03 19:13:05 +02:00
|
|
|
MusicusClient client;
|
2020-06-02 16:47:46 +02:00
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
int get schemaVersion => 1;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
MigrationStrategy get migration => MigrationStrategy(
|
|
|
|
|
beforeOpen: (details) async {
|
2020-07-20 18:39:28 +02:00
|
|
|
await customStatement('PRAGMA foreign_keys = ON;');
|
2020-06-02 16:47:46 +02:00
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
MusicusClientDatabase({
|
|
|
|
|
@required QueryExecutor executor,
|
|
|
|
|
this.client,
|
|
|
|
|
}) : super(executor);
|
|
|
|
|
|
|
|
|
|
MusicusClientDatabase.connect({
|
|
|
|
|
@required DatabaseConnection connection,
|
|
|
|
|
this.client,
|
|
|
|
|
}) : super.connect(connection);
|
|
|
|
|
|
2020-06-03 18:37:20 +02:00
|
|
|
/// Upload all changes to the server.
|
|
|
|
|
///
|
|
|
|
|
/// If [update] is true, this will also update existing items with new data
|
|
|
|
|
/// from the server.
|
|
|
|
|
Future<void> sync([bool update = false]) async {
|
|
|
|
|
if (update) {
|
|
|
|
|
for (final person in await oldSyncPersons().get()) {
|
|
|
|
|
await updatePerson(await client.getPerson(person.id));
|
|
|
|
|
}
|
|
|
|
|
for (final instrument in await oldSyncInstruments().get()) {
|
|
|
|
|
await updateInstrument(await client.getInstrument(instrument.id));
|
|
|
|
|
}
|
|
|
|
|
for (final work in await oldSyncWorks().get()) {
|
|
|
|
|
final workInfo = await client.getWork(work.id);
|
|
|
|
|
await updateWork(workInfo);
|
|
|
|
|
}
|
|
|
|
|
for (final ensemble in await oldSyncEnsembles().get()) {
|
|
|
|
|
await updateEnsemble(await client.getEnsemble(ensemble.id));
|
|
|
|
|
}
|
|
|
|
|
for (final recording in await oldSyncRecordings().get()) {
|
|
|
|
|
final recordingInfo = await client.getRecording(recording.id);
|
|
|
|
|
await updateRecording(recordingInfo);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (final person in await newSyncPersons().get()) {
|
|
|
|
|
await client.putPerson(person);
|
2020-06-03 20:03:35 +02:00
|
|
|
await updatePerson(person.copyWith(synced: true));
|
2020-06-03 18:37:20 +02:00
|
|
|
}
|
|
|
|
|
for (final instrument in await newSyncInstruments().get()) {
|
|
|
|
|
await client.putInstrument(instrument);
|
2020-06-03 20:03:35 +02:00
|
|
|
await updateInstrument(instrument.copyWith(synced: true));
|
2020-06-03 18:37:20 +02:00
|
|
|
}
|
|
|
|
|
for (final work in await newSyncWorks().get()) {
|
|
|
|
|
final workInfo = await getWorkInfo(work);
|
|
|
|
|
await client.putWork(workInfo);
|
2020-06-03 20:03:35 +02:00
|
|
|
await into(works).insertOnConflictUpdate(work.copyWith(synced: true));
|
2020-06-03 18:37:20 +02:00
|
|
|
}
|
|
|
|
|
for (final ensemble in await newSyncEnsembles().get()) {
|
|
|
|
|
await client.putEnsemble(ensemble);
|
2020-06-03 20:03:35 +02:00
|
|
|
await updateEnsemble(ensemble.copyWith(synced: true));
|
2020-06-03 18:37:20 +02:00
|
|
|
}
|
|
|
|
|
for (final recording in await newSyncRecordings().get()) {
|
|
|
|
|
final recordingInfo = await getRecordingInfo(recording);
|
|
|
|
|
await client.putRecording(recordingInfo);
|
2020-06-03 20:03:35 +02:00
|
|
|
await into(recordings)
|
|
|
|
|
.insertOnConflictUpdate(recording.copyWith(synced: true));
|
2020-06-03 18:37:20 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-02 16:47:46 +02:00
|
|
|
/// Get all available persons.
|
|
|
|
|
///
|
|
|
|
|
/// This will return a list of [pageSize] persons. You can get another page
|
|
|
|
|
/// using the [page] parameter. If a non empty [search] string is provided,
|
|
|
|
|
/// the persons will get filtered based on that string.
|
|
|
|
|
Future<List<Person>> getPersons([int page, String search]) async {
|
|
|
|
|
final offset = page != null ? page * pageSize : 0;
|
|
|
|
|
List<Person> result;
|
|
|
|
|
|
|
|
|
|
if (search == null || search.isEmpty) {
|
|
|
|
|
result = await allPersons(pageSize, offset).get();
|
|
|
|
|
} else {
|
|
|
|
|
result = await searchPersons('$search%', pageSize, offset).get();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Add [person] or replace an existing person with the same ID.
|
|
|
|
|
Future<void> updatePerson(Person person) async {
|
|
|
|
|
await into(persons).insert(
|
|
|
|
|
person,
|
|
|
|
|
mode: InsertMode.insertOrReplace,
|
|
|
|
|
);
|
2020-06-03 18:37:20 +02:00
|
|
|
}
|
2020-06-02 16:47:46 +02:00
|
|
|
|
|
|
|
|
/// Delete the person by [id].
|
2020-06-03 18:37:20 +02:00
|
|
|
///
|
2020-06-03 10:05:58 +02:00
|
|
|
/// If [sync] is true, the person will be deleted from the server too. If
|
|
|
|
|
/// that fails, a MusicusNotAuthorizedException or MusicusNotLoggedInException
|
|
|
|
|
/// willl be thrown and the person will NOT be deleted.
|
|
|
|
|
Future<void> deletePerson(int id, [bool sync = false]) async {
|
|
|
|
|
if (sync) {
|
|
|
|
|
await client.deletePerson(id);
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-02 16:47:46 +02:00
|
|
|
await (delete(persons)..where((p) => p.id.equals(id))).go();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get all available instruments.
|
|
|
|
|
///
|
|
|
|
|
/// This will return a list of [pageSize] instruments. You can get another
|
|
|
|
|
/// page using the [page] parameter. If a non empty [search] string is
|
|
|
|
|
/// provided, the instruments will get filtered based on that string.
|
|
|
|
|
Future<List<Instrument>> getInstruments([int page, String search]) async {
|
|
|
|
|
final offset = page != null ? page * pageSize : 0;
|
|
|
|
|
List<Instrument> result;
|
|
|
|
|
|
|
|
|
|
if (search == null || search.isEmpty) {
|
|
|
|
|
result = await allInstruments(pageSize, offset).get();
|
|
|
|
|
} else {
|
|
|
|
|
result = await searchInstruments('$search%', pageSize, offset).get();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Add [instrument] or replace an existing one with the same ID.
|
|
|
|
|
Future<void> updateInstrument(Instrument instrument) async {
|
|
|
|
|
await into(instruments).insert(
|
|
|
|
|
instrument,
|
|
|
|
|
mode: InsertMode.insertOrReplace,
|
|
|
|
|
);
|
2020-06-03 18:37:20 +02:00
|
|
|
}
|
2020-06-02 16:47:46 +02:00
|
|
|
|
|
|
|
|
/// Delete the instrument by [id].
|
2020-06-03 18:37:20 +02:00
|
|
|
///
|
2020-06-03 10:05:58 +02:00
|
|
|
/// If [sync] is true, the instrument will be deleted from the server too. If
|
|
|
|
|
/// that fails, a MusicusNotAuthorizedException or MusicusNotLoggedInException
|
|
|
|
|
/// willl be thrown and the instrument will NOT be deleted.
|
|
|
|
|
Future<void> deleteInstrument(int id, [bool sync = false]) async {
|
|
|
|
|
if (sync) {
|
2020-07-20 17:24:42 +02:00
|
|
|
await client.deleteInstrument(id);
|
2020-06-03 10:05:58 +02:00
|
|
|
}
|
|
|
|
|
|
2020-06-02 16:47:46 +02:00
|
|
|
await (delete(instruments)..where((i) => i.id.equals(id))).go();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Retrieve more information on an already queried work.
|
|
|
|
|
Future<WorkInfo> getWorkInfo(Work work) async {
|
|
|
|
|
final id = work.id;
|
|
|
|
|
|
|
|
|
|
final composers = await partComposersByWork(id).get();
|
|
|
|
|
composers.insert(0, await personById(work.composer).getSingle());
|
|
|
|
|
final instruments = await instrumentsByWork(id).get();
|
|
|
|
|
|
|
|
|
|
final List<PartInfo> parts = [];
|
|
|
|
|
for (final part in await partsByWork(id).get()) {
|
|
|
|
|
parts.add(PartInfo(
|
|
|
|
|
part: part,
|
|
|
|
|
composer: part.composer != null
|
|
|
|
|
? await personById(part.composer).getSingle()
|
|
|
|
|
: null,
|
|
|
|
|
instruments: await instrumentsByWorkPart(part.id).get(),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final List<WorkSection> sections = [];
|
|
|
|
|
for (final section in await sectionsByWork(id).get()) {
|
|
|
|
|
sections.add(section);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return WorkInfo(
|
|
|
|
|
work: work,
|
|
|
|
|
instruments: instruments,
|
|
|
|
|
composers: composers,
|
|
|
|
|
parts: parts,
|
|
|
|
|
sections: sections,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get all available information on a work.
|
|
|
|
|
Future<WorkInfo> getWork(int id) async {
|
|
|
|
|
final work = await workById(id).getSingle();
|
|
|
|
|
return await getWorkInfo(work);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get information on all works written by the person with ID [personId].
|
|
|
|
|
///
|
|
|
|
|
/// This will return a list of [pageSize] results. You can get another page
|
|
|
|
|
/// using the [page] parameter. If a non empty [search] string is provided,
|
|
|
|
|
/// the works will be filtered using that string.
|
|
|
|
|
Future<List<WorkInfo>> getWorks(int personId,
|
|
|
|
|
[int page, String search]) async {
|
|
|
|
|
final offset = page != null ? page * pageSize : 0;
|
|
|
|
|
List<Work> works;
|
|
|
|
|
|
|
|
|
|
if (search == null || search.isEmpty) {
|
|
|
|
|
works = await worksByComposer(personId, pageSize, offset).get();
|
|
|
|
|
} else {
|
|
|
|
|
works =
|
|
|
|
|
await searchWorksByComposer(personId, '$search%', pageSize, offset)
|
|
|
|
|
.get();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final List<WorkInfo> result = [];
|
|
|
|
|
for (final work in works) {
|
|
|
|
|
result.add(await getWorkInfo(work));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Add or replace a work and its associated data.
|
|
|
|
|
///
|
|
|
|
|
/// This will explicitly update all associated composers and instruments, even
|
|
|
|
|
/// if they have already existed before.
|
|
|
|
|
Future<void> updateWork(WorkInfo workInfo) async {
|
|
|
|
|
await transaction(() async {
|
|
|
|
|
final workId = workInfo.work.id;
|
|
|
|
|
|
2020-07-20 18:44:34 +02:00
|
|
|
// Delete data associated rows in other tables first. We can't just
|
|
|
|
|
// delete the work itself, because that would violate the foreign key
|
|
|
|
|
// constraints.
|
|
|
|
|
await (delete(instrumentations)..where((i) => i.work.equals(workId)))
|
|
|
|
|
.go();
|
|
|
|
|
await (delete(workParts)..where((p) => p.partOf.equals(workId))).go();
|
|
|
|
|
await (delete(workSections)..where((s) => s.work.equals(workId))).go();
|
2020-06-02 16:47:46 +02:00
|
|
|
|
|
|
|
|
// This will also include the composers of the work's parts.
|
|
|
|
|
for (final person in workInfo.composers) {
|
|
|
|
|
await updatePerson(person);
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-20 18:44:34 +02:00
|
|
|
await into(works).insert(workInfo.work, mode: InsertMode.insertOrReplace);
|
2020-06-02 16:47:46 +02:00
|
|
|
|
|
|
|
|
// At the moment, this will also update all provided instruments, even if
|
|
|
|
|
// they were already there previously.
|
|
|
|
|
for (final instrument in workInfo.instruments) {
|
|
|
|
|
await updateInstrument(instrument);
|
|
|
|
|
await into(instrumentations).insert(Instrumentation(
|
|
|
|
|
work: workId,
|
|
|
|
|
instrument: instrument.id,
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (final partInfo in workInfo.parts) {
|
|
|
|
|
final part = partInfo.part;
|
|
|
|
|
|
|
|
|
|
await into(workParts).insert(part);
|
|
|
|
|
|
|
|
|
|
for (final instrument in workInfo.instruments) {
|
|
|
|
|
await updateInstrument(instrument);
|
|
|
|
|
await into(partInstrumentations).insert(PartInstrumentation(
|
|
|
|
|
workPart: part.id,
|
|
|
|
|
instrument: instrument.id,
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (final section in workInfo.sections) {
|
|
|
|
|
await into(workSections).insert(section);
|
|
|
|
|
}
|
|
|
|
|
});
|
2020-06-03 18:37:20 +02:00
|
|
|
}
|
2020-06-02 16:47:46 +02:00
|
|
|
|
|
|
|
|
/// Delete the work by [id].
|
2020-06-03 18:37:20 +02:00
|
|
|
///
|
2020-06-03 10:05:58 +02:00
|
|
|
/// If [sync] is true, the work will be deleted from the server too. If
|
|
|
|
|
/// that fails, a MusicusNotAuthorizedException or MusicusNotLoggedInException
|
|
|
|
|
/// willl be thrown and the work will NOT be deleted.
|
|
|
|
|
Future<void> deleteWork(int id, [bool sync = false]) async {
|
|
|
|
|
if (sync) {
|
2020-07-20 17:24:42 +02:00
|
|
|
await client.deleteWork(id);
|
2020-06-03 10:05:58 +02:00
|
|
|
}
|
|
|
|
|
|
2020-06-02 16:47:46 +02:00
|
|
|
// The parts and instrumentations will be deleted automatically due to
|
|
|
|
|
// their foreign key constraints.
|
|
|
|
|
await (delete(works)..where((w) => w.id.equals(id))).go();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get all available ensembles.
|
|
|
|
|
///
|
|
|
|
|
/// This will return a list of [pageSize] ensembles. You can get another page
|
|
|
|
|
/// using the [page] parameter. If a non empty [search] string is provided,
|
|
|
|
|
/// the ensembles will get filtered based on that string.
|
|
|
|
|
Future<List<Ensemble>> getEnsembles([int page, String search]) async {
|
|
|
|
|
final offset = page != null ? page * pageSize : 0;
|
|
|
|
|
List<Ensemble> result;
|
|
|
|
|
|
|
|
|
|
if (search == null || search.isEmpty) {
|
|
|
|
|
result = await allEnsembles(pageSize, offset).get();
|
|
|
|
|
} else {
|
|
|
|
|
result = await searchEnsembles('$search%', pageSize, offset).get();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Add [ensemble] or replace an existing one with the same ID.
|
|
|
|
|
Future<void> updateEnsemble(Ensemble ensemble) async {
|
|
|
|
|
await into(ensembles).insert(
|
|
|
|
|
ensemble,
|
|
|
|
|
mode: InsertMode.insertOrReplace,
|
|
|
|
|
);
|
2020-06-03 18:37:20 +02:00
|
|
|
}
|
2020-06-02 16:47:46 +02:00
|
|
|
|
|
|
|
|
/// Delete the ensemble by [id].
|
2020-06-03 18:37:20 +02:00
|
|
|
///
|
2020-06-03 10:05:58 +02:00
|
|
|
/// If [sync] is true, the ensemble will be deleted from the server too. If
|
|
|
|
|
/// that fails, a MusicusNotAuthorizedException or MusicusNotLoggedInException
|
|
|
|
|
/// willl be thrown and the ensemble will NOT be deleted.
|
|
|
|
|
Future<void> deleteEnsemble(int id, [bool sync = false]) async {
|
|
|
|
|
if (sync) {
|
2020-07-20 17:24:42 +02:00
|
|
|
await client.deleteEnsemble(id);
|
2020-06-03 10:05:58 +02:00
|
|
|
}
|
|
|
|
|
|
2020-06-02 16:47:46 +02:00
|
|
|
await (delete(ensembles)..where((e) => e.id.equals(id))).go();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Add or replace a recording and its associated data.
|
|
|
|
|
///
|
|
|
|
|
/// This will explicitly also update all assoicated persons and instruments.
|
|
|
|
|
Future<void> updateRecording(RecordingInfo recordingInfo) async {
|
|
|
|
|
await transaction(() async {
|
|
|
|
|
final recordingId = recordingInfo.recording.id;
|
|
|
|
|
|
|
|
|
|
// Delete the old recording first. This will also delete the performances
|
|
|
|
|
// due to their foreign key constraint.
|
|
|
|
|
await deleteRecording(recordingId);
|
|
|
|
|
|
|
|
|
|
await into(recordings).insert(recordingInfo.recording);
|
|
|
|
|
|
|
|
|
|
for (final performance in recordingInfo.performances) {
|
|
|
|
|
if (performance.person != null) {
|
|
|
|
|
await updatePerson(performance.person);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (performance.ensemble != null) {
|
|
|
|
|
await updateEnsemble(performance.ensemble);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (performance.role != null) {
|
|
|
|
|
await updateInstrument(performance.role);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await into(performances).insert(Performance(
|
|
|
|
|
recording: recordingId,
|
|
|
|
|
person: performance.person?.id,
|
|
|
|
|
ensemble: performance.ensemble?.id,
|
|
|
|
|
role: performance.role?.id,
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Retreive more information on an already queried recording.
|
|
|
|
|
Future<RecordingInfo> getRecordingInfo(Recording recording) async {
|
|
|
|
|
final id = recording.id;
|
|
|
|
|
|
|
|
|
|
final List<PerformanceInfo> performances = [];
|
|
|
|
|
for (final performance in await performancesByRecording(id).get()) {
|
|
|
|
|
performances.add(PerformanceInfo(
|
|
|
|
|
person: performance.person != null
|
|
|
|
|
? await personById(performance.person).getSingle()
|
|
|
|
|
: null,
|
|
|
|
|
ensemble: performance.ensemble != null
|
|
|
|
|
? await ensembleById(performance.ensemble).getSingle()
|
|
|
|
|
: null,
|
|
|
|
|
role: performance.role != null
|
|
|
|
|
? await instrumentById(performance.role).getSingle()
|
|
|
|
|
: null,
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return RecordingInfo(
|
|
|
|
|
recording: recording,
|
|
|
|
|
performances: performances,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get all available information on a recording.
|
|
|
|
|
Future<RecordingInfo> getRecording(int id) async {
|
|
|
|
|
final recording = await recordingById(id).getSingle();
|
|
|
|
|
return await getRecordingInfo(recording);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Delete a recording by [id].
|
2020-06-03 18:37:20 +02:00
|
|
|
///
|
2020-06-03 10:05:58 +02:00
|
|
|
/// If [sync] is true, the recording will be deleted from the server too. If
|
|
|
|
|
/// that fails, a MusicusNotAuthorizedException or MusicusNotLoggedInException
|
|
|
|
|
/// willl be thrown and the recording will NOT be deleted.
|
|
|
|
|
Future<void> deleteRecording(int id, [bool sync = false]) async {
|
|
|
|
|
if (sync) {
|
2020-07-20 17:24:42 +02:00
|
|
|
await client.deleteRecording(id);
|
2020-06-03 10:05:58 +02:00
|
|
|
}
|
|
|
|
|
|
2020-06-02 16:47:46 +02:00
|
|
|
// This will also delete the performances due to their foreign key
|
|
|
|
|
// constraint.
|
|
|
|
|
await (delete(recordings)..where((r) => r.id.equals(id))).go();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get information on all recordings of the work with ID [workId].
|
|
|
|
|
///
|
|
|
|
|
/// This will return a list of [pageSize] recordings. You can get the other
|
|
|
|
|
/// pages using the [page] parameter.
|
|
|
|
|
Future<List<RecordingInfo>> getRecordings(int workId, [int page]) async {
|
|
|
|
|
final offset = page != null ? page * pageSize : 0;
|
|
|
|
|
final recordings = await recordingsByWork(workId, pageSize, offset).get();
|
|
|
|
|
|
|
|
|
|
final List<RecordingInfo> result = [];
|
|
|
|
|
for (final recording in recordings) {
|
|
|
|
|
result.add(await getRecordingInfo(recording));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
}
|