@finos/legend-studio
Version:
908 lines (834 loc) • 27.9 kB
text/typescript
/**
* Copyright (c) 2020-present, Goldman Sachs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { observable, action, flow, makeObservable, flowResult } from 'mobx';
import {
type GeneratorFn,
ActionState,
assertErrorThrown,
LogEvent,
losslessStringify,
UnsupportedOperationError,
filterByType,
} from '@finos/legend-shared';
import type { ServiceEditorState } from './ServiceEditorState.js';
import {
decorateRuntimeWithNewMapping,
RuntimeEditorState,
} from '../../../editor-state/element-editor-state/RuntimeEditorState.js';
import {
buildParametersLetLambdaFunc,
ExecutionPlanState,
LambdaEditorState,
LambdaParametersState,
LambdaParameterState,
PARAMETER_SUBMIT_ACTION,
TAB_SIZE,
} from '@finos/legend-application';
import {
type ServiceExecution,
KeyedExecutionParameter,
type PureExecution,
type Mapping,
type Runtime,
type ExecutionResult,
type LightQuery,
type PackageableRuntime,
PureSingleExecution,
PureMultiExecution,
type RawExecutionPlan,
type PackageableElementReference,
GRAPH_MANAGER_EVENT,
RawLambda,
EngineRuntime,
RuntimePointer,
PackageableElementExplicitReference,
buildSourceInformationSourceId,
QueryProjectCoordinates,
QuerySearchSpecification,
buildLambdaVariableExpressions,
observe_ValueSpecification,
VariableExpression,
buildRawLambdaFromLambdaFunction,
stub_PackageableRuntime,
stub_Mapping,
} from '@finos/legend-graph';
import type { Entity } from '@finos/legend-model-storage';
import { parseGACoordinates } from '@finos/legend-server-depot';
import { runtime_addMapping } from '../../../graphModifier/DSLMapping_GraphModifierHelper.js';
import type { EditorStore } from '../../../EditorStore.js';
import {
keyedExecutionParameter_setKey,
pureExecution_setFunction,
pureMultiExecution_addExecutionParameter,
pureMultiExecution_deleteExecutionParameter,
pureMultiExecution_setExecutionKey,
pureSingleExecution_setMapping,
pureSingleExecution_setRuntime,
service_setExecution,
} from '../../../graphModifier/DSLService_GraphModifierHelper.js';
export class ServiceExecutionParameterState extends LambdaParametersState {
executionState: ServicePureExecutionState;
constructor(executionState: ServicePureExecutionState) {
super();
makeObservable(this, {
parameterValuesEditorState: observable,
parameterStates: observable,
addParameter: action,
removeParameter: action,
openModal: action,
build: action,
setParameters: action,
});
this.executionState = executionState;
}
openModal(query: RawLambda): void {
this.parameterStates = this.build(query);
this.parameterValuesEditorState.open(
(): Promise<void> =>
flowResult(this.executionState.execute()).catch(
this.executionState.editorStore.applicationStore.alertUnhandledError,
),
PARAMETER_SUBMIT_ACTION.EXECUTE,
);
}
build(query: RawLambda): LambdaParameterState[] {
const parameters = buildLambdaVariableExpressions(
query,
this.executionState.editorStore.graphManagerState,
)
.map((p) =>
observe_ValueSpecification(
p,
this.executionState.editorStore.changeDetectionState.observerContext,
),
)
.filter(filterByType(VariableExpression));
const states = parameters.map((p) => {
const parmeterState = new LambdaParameterState(
p,
this.executionState.editorStore.changeDetectionState.observerContext,
);
parmeterState.mockParameterValue();
return parmeterState;
});
return states;
}
}
export abstract class ServiceExecutionState {
editorStore: EditorStore;
serviceEditorState: ServiceEditorState;
execution: ServiceExecution;
constructor(
editorStore: EditorStore,
serviceEditorState: ServiceEditorState,
execution: ServiceExecution,
) {
makeObservable(this, {
execution: observable,
});
this.editorStore = editorStore;
this.execution = execution;
this.serviceEditorState = serviceEditorState;
}
abstract get serviceExecutionParameters():
| { query: RawLambda; mapping: Mapping; runtime: Runtime }
| undefined;
}
export class UnsupportedServiceExecutionState extends ServiceExecutionState {
get serviceExecutionParameters():
| { query: RawLambda; mapping: Mapping; runtime: Runtime }
| undefined {
return undefined;
}
}
interface QueryImportInfo {
query: LightQuery;
content: string;
}
export class ServicePureExecutionQueryState extends LambdaEditorState {
editorStore: EditorStore;
execution: PureExecution;
isInitializingLambda = false;
openQueryImporter = false;
queries: LightQuery[] = [];
selectedQueryInfo?: QueryImportInfo | undefined;
loadQueriesState = ActionState.create();
loadQueryInfoState = ActionState.create();
importQueryState = ActionState.create();
constructor(editorStore: EditorStore, execution: PureExecution) {
super('', '');
makeObservable(this, {
execution: observable,
isInitializingLambda: observable,
openQueryImporter: observable,
queries: observable,
selectedQueryInfo: observable,
setOpenQueryImporter: action,
setIsInitializingLambda: action,
setLambda: action,
updateLamba: flow,
loadQueries: flow,
setSelectedQueryInfo: flow,
importQuery: flow,
});
this.editorStore = editorStore;
this.execution = execution;
}
get lambdaId(): string {
return buildSourceInformationSourceId([
this.execution._OWNER.path,
'execution',
]);
}
get query(): RawLambda {
return this.execution.func;
}
setIsInitializingLambda(val: boolean): void {
this.isInitializingLambda = val;
}
setLambda(val: RawLambda): void {
pureExecution_setFunction(this.execution, val);
}
setOpenQueryImporter(val: boolean): void {
this.openQueryImporter = val;
}
*setSelectedQueryInfo(query: LightQuery | undefined): GeneratorFn<void> {
if (query) {
try {
this.loadQueryInfoState.inProgress();
const content =
(yield this.editorStore.graphManagerState.graphManager.lambdaToPureCode(
(yield this.editorStore.graphManagerState.graphManager.pureCodeToLambda(
(yield this.editorStore.graphManagerState.graphManager.getQueryContent(
query.id,
)) as string,
)) as RawLambda,
true,
)) as string;
this.selectedQueryInfo = {
query,
content,
};
} catch (error) {
assertErrorThrown(error);
this.editorStore.applicationStore.notifyError(error);
} finally {
this.loadQueryInfoState.reset();
}
} else {
this.selectedQueryInfo = undefined;
}
}
*importQuery(): GeneratorFn<void> {
if (this.selectedQueryInfo) {
try {
this.importQueryState.inProgress();
const lambda =
(yield this.editorStore.graphManagerState.graphManager.pureCodeToLambda(
this.selectedQueryInfo.content,
)) as RawLambda;
yield flowResult(this.updateLamba(lambda));
} catch (error) {
assertErrorThrown(error);
this.editorStore.applicationStore.notifyError(error);
} finally {
this.setOpenQueryImporter(false);
this.importQueryState.reset();
}
}
}
*loadQueries(searchText: string): GeneratorFn<void> {
const isValidSearchString = searchText.length >= 3;
this.loadQueriesState.inProgress();
try {
const searchSpecification = new QuerySearchSpecification();
const currentProjectCoordinates = new QueryProjectCoordinates();
currentProjectCoordinates.groupId =
this.editorStore.projectConfigurationEditorState.currentProjectConfiguration.groupId;
currentProjectCoordinates.artifactId =
this.editorStore.projectConfigurationEditorState.currentProjectConfiguration.artifactId;
searchSpecification.searchTerm = isValidSearchString
? searchText
: undefined;
searchSpecification.limit = 10;
searchSpecification.projectCoordinates = [
// either get queries for the current project
currentProjectCoordinates,
// or any of its dependencies
...Array.from(
(
(yield flowResult(
this.editorStore.graphState.getIndexedDependencyEntities(),
)) as Map<string, Entity[]>
).keys(),
).map((coordinatesInText) => {
const { groupId, artifactId } = parseGACoordinates(coordinatesInText);
const coordinates = new QueryProjectCoordinates();
coordinates.groupId = groupId;
coordinates.artifactId = artifactId;
return coordinates;
}),
];
this.queries =
(yield this.editorStore.graphManagerState.graphManager.searchQueries(
searchSpecification,
)) as LightQuery[];
this.loadQueriesState.pass();
} catch (error) {
assertErrorThrown(error);
this.loadQueriesState.fail();
this.editorStore.applicationStore.notifyError(error);
}
}
*updateLamba(val: RawLambda): GeneratorFn<void> {
this.setLambda(val);
yield flowResult(this.convertLambdaObjectToGrammarString(true));
}
*convertLambdaObjectToGrammarString(pretty?: boolean): GeneratorFn<void> {
if (this.execution.func.body) {
try {
const lambdas = new Map<string, RawLambda>();
lambdas.set(
this.lambdaId,
new RawLambda(
this.execution.func.parameters,
this.execution.func.body,
),
);
const isolatedLambdas =
(yield this.editorStore.graphManagerState.graphManager.lambdasToPureCode(
lambdas,
pretty,
)) as Map<string, string>;
const grammarText = isolatedLambdas.get(this.lambdaId);
this.setLambdaString(
grammarText !== undefined
? this.extractLambdaString(grammarText)
: '',
);
this.clearErrors();
} catch (error) {
assertErrorThrown(error);
this.editorStore.applicationStore.log.error(
LogEvent.create(GRAPH_MANAGER_EVENT.PARSING_FAILURE),
error,
);
}
} else {
this.clearErrors();
this.setLambdaString('');
}
}
// NOTE: since we don't allow edition in text mode, we don't need to implement this
*convertLambdaGrammarStringToObject(): GeneratorFn<void> {
throw new UnsupportedOperationError();
}
}
export interface ServiceExecutionContext {
mapping: PackageableElementReference<Mapping>;
runtime: Runtime;
}
export abstract class ServiceExecutionContextState {
executionContext: ServiceExecutionContext;
executionState: ServiceExecutionState;
constructor(
executionContext: ServiceExecutionContext,
executionState: ServiceExecutionState,
) {
this.executionContext = executionContext;
this.executionState = executionState;
}
abstract setMapping(value: Mapping): void;
abstract setRuntime(value: Runtime): void;
}
export class SingleExecutionContextState extends ServiceExecutionContextState {
declare executionContext: PureSingleExecution;
constructor(
executionContext: PureSingleExecution,
executionState: ServiceExecutionState,
) {
super(executionContext, executionState);
makeObservable(this, {
executionContext: observable,
setMapping: action,
setRuntime: action,
});
}
setMapping(value: Mapping): void {
pureSingleExecution_setMapping(
this.executionContext,
value,
this.executionState.editorStore.changeDetectionState.observerContext,
);
}
setRuntime(value: Runtime): void {
pureSingleExecution_setRuntime(
this.executionContext,
value,
this.executionState.editorStore.changeDetectionState.observerContext,
);
}
}
export class KeyedExecutionContextState extends ServiceExecutionContextState {
declare executionContext: KeyedExecutionParameter;
setMapping(value: Mapping): void {
pureSingleExecution_setMapping(
this.executionContext,
value,
this.executionState.editorStore.changeDetectionState.observerContext,
);
}
setRuntime(value: Runtime): void {
pureSingleExecution_setRuntime(
this.executionContext,
value,
this.executionState.editorStore.changeDetectionState.observerContext,
);
}
}
export abstract class ServicePureExecutionState extends ServiceExecutionState {
queryState: ServicePureExecutionQueryState;
declare execution: PureExecution;
selectedExecutionContextState: ServiceExecutionContextState | undefined;
runtimeEditorState?: RuntimeEditorState | undefined;
isExecuting = false;
isGeneratingPlan = false;
isOpeningQueryEditor = false;
executionResultText?: string | undefined; // NOTE: stored as lossless JSON string
executionPlanState: ExecutionPlanState;
parameterState: ServiceExecutionParameterState;
showChangeExecModal = false;
constructor(
editorStore: EditorStore,
serviceEditorState: ServiceEditorState,
execution: PureExecution,
) {
super(editorStore, serviceEditorState, execution);
this.execution = execution;
this.queryState = new ServicePureExecutionQueryState(
this.editorStore,
execution,
);
this.executionPlanState = new ExecutionPlanState(
this.editorStore.applicationStore,
this.editorStore.graphManagerState,
);
this.parameterState = new ServiceExecutionParameterState(this);
}
abstract changeExecution(): void;
isChangeExecutionDisabled(): boolean {
return false;
}
setShowChangeExecModal(val: boolean): void {
this.showChangeExecModal = val;
}
setOpeningQueryEditor(val: boolean): void {
this.isOpeningQueryEditor = val;
}
setExecutionResultText = (executionResult: string | undefined): void => {
this.executionResultText = executionResult;
};
setQueryState = (queryState: ServicePureExecutionQueryState): void => {
this.queryState = queryState;
};
*generatePlan(debug: boolean): GeneratorFn<void> {
if (!this.selectedExecutionContextState || this.isGeneratingPlan) {
return;
}
try {
const query = this.queryState.query;
this.isGeneratingPlan = true;
let rawPlan: RawExecutionPlan;
if (debug) {
const debugResult =
(yield this.editorStore.graphManagerState.graphManager.debugExecutionPlanGeneration(
query,
this.selectedExecutionContextState.executionContext.mapping.value,
this.selectedExecutionContextState.executionContext.runtime,
this.editorStore.graphManagerState.graph,
)) as { plan: RawExecutionPlan; debug: string };
rawPlan = debugResult.plan;
this.executionPlanState.setDebugText(debugResult.debug);
} else {
rawPlan =
(yield this.editorStore.graphManagerState.graphManager.generateExecutionPlan(
query,
this.selectedExecutionContextState.executionContext.mapping.value,
this.selectedExecutionContextState.executionContext.runtime,
this.editorStore.graphManagerState.graph,
)) as object;
}
try {
this.executionPlanState.setRawPlan(rawPlan);
const plan =
this.editorStore.graphManagerState.graphManager.buildExecutionPlan(
rawPlan,
this.editorStore.graphManagerState.graph,
);
this.executionPlanState.setPlan(plan);
} catch {
// do nothing
}
} catch (error) {
assertErrorThrown(error);
this.editorStore.applicationStore.log.error(
LogEvent.create(GRAPH_MANAGER_EVENT.EXECUTION_FAILURE),
error,
);
this.editorStore.applicationStore.notifyError(error);
} finally {
this.isGeneratingPlan = false;
}
}
*handleExecute(): GeneratorFn<void> {
if (!this.selectedExecutionContextState || this.isExecuting) {
return;
}
const query = this.queryState.query;
const parameters = (query.parameters ?? []) as object[];
if (parameters.length) {
this.parameterState.openModal(query);
} else {
this.execute();
}
}
*execute(): GeneratorFn<void> {
if (!this.selectedExecutionContextState || this.isExecuting) {
return;
}
try {
this.isExecuting = true;
const query = this.getExecutionQuery();
const result =
(yield this.editorStore.graphManagerState.graphManager.executeMapping(
query,
this.selectedExecutionContextState.executionContext.mapping.value,
this.selectedExecutionContextState.executionContext.runtime,
this.editorStore.graphManagerState.graph,
{
useLosslessParse: true,
},
)) as ExecutionResult;
this.setExecutionResultText(
losslessStringify(result, undefined, TAB_SIZE),
);
this.parameterState.setParameters([]);
} catch (error) {
assertErrorThrown(error);
this.editorStore.applicationStore.log.error(
LogEvent.create(GRAPH_MANAGER_EVENT.EXECUTION_FAILURE),
error,
);
this.editorStore.applicationStore.notifyError(error);
} finally {
this.isExecuting = false;
}
}
getExecutionQuery(): RawLambda {
if (this.parameterState.parameterStates.length) {
const letlambdaFunction = buildParametersLetLambdaFunc(
this.editorStore.graphManagerState.graph,
this.parameterState.parameterStates,
);
const letRawLambda = buildRawLambdaFromLambdaFunction(
letlambdaFunction,
this.editorStore.graphManagerState,
);
// reset parameters
if (
Array.isArray(this.queryState.query.body) &&
Array.isArray(letRawLambda.body)
) {
letRawLambda.body = [
...(letRawLambda.body as object[]),
...(this.queryState.query.body as object[]),
];
return letRawLambda;
}
}
return this.queryState.query;
}
get serviceExecutionParameters():
| { query: RawLambda; mapping: Mapping; runtime: Runtime }
| undefined {
if (!this.selectedExecutionContextState || this.isExecuting) {
return undefined;
}
const query = this.queryState.query;
return {
query,
mapping:
this.selectedExecutionContextState.executionContext.mapping.value,
runtime: this.selectedExecutionContextState.executionContext.runtime,
};
}
closeRuntimeEditor(): void {
this.runtimeEditorState = undefined;
}
openRuntimeEditor(): void {
if (
this.selectedExecutionContextState &&
!(
this.selectedExecutionContextState.executionContext.runtime instanceof
RuntimePointer
)
) {
this.runtimeEditorState = new RuntimeEditorState(
this.editorStore,
this.selectedExecutionContextState.executionContext.runtime,
true,
);
}
}
useCustomRuntime(): void {
if (this.selectedExecutionContextState) {
const customRuntime = new EngineRuntime();
runtime_addMapping(
customRuntime,
PackageableElementExplicitReference.create(
this.selectedExecutionContextState.executionContext.mapping.value,
),
);
decorateRuntimeWithNewMapping(
this.selectedExecutionContextState.executionContext.runtime,
this.selectedExecutionContextState.executionContext.mapping.value,
this.editorStore,
);
this.selectedExecutionContextState.setRuntime(customRuntime);
}
}
autoSelectRuntimeOnMappingChange(mapping: Mapping): void {
if (this.selectedExecutionContextState) {
const runtimes =
this.editorStore.graphManagerState.graph.ownRuntimes.filter((runtime) =>
runtime.runtimeValue.mappings.map((m) => m.value).includes(mapping),
);
if (runtimes.length) {
this.selectedExecutionContextState.setRuntime(
(runtimes[0] as PackageableRuntime).runtimeValue,
);
} else {
this.useCustomRuntime();
}
}
}
abstract getInitiallySelectedExecutionContextState():
| ServiceExecutionContextState
| undefined;
updateExecutionQuery(): void {
pureExecution_setFunction(this.execution, this.queryState.query);
}
}
export class SingleServicePureExecutionState extends ServicePureExecutionState {
declare execution: PureSingleExecution;
declare selectedExecutionContextState: ServiceExecutionContextState;
multiExecutionKey = 'key';
constructor(
editorStore: EditorStore,
serviceEditorState: ServiceEditorState,
execution: PureSingleExecution,
) {
super(editorStore, serviceEditorState, execution);
makeObservable(this, {
queryState: observable,
getInitiallySelectedExecutionContextState: observable,
runtimeEditorState: observable,
isExecuting: observable,
isGeneratingPlan: observable,
isOpeningQueryEditor: observable,
executionResultText: observable,
executionPlanState: observable,
showChangeExecModal: observable,
parameterState: observable,
multiExecutionKey: observable,
setExecutionResultText: action,
closeRuntimeEditor: action,
openRuntimeEditor: action,
useCustomRuntime: action,
setQueryState: action,
autoSelectRuntimeOnMappingChange: action,
updateExecutionQuery: action,
setOpeningQueryEditor: action,
changeExecution: action,
setMultiExecutionKey: action,
setShowChangeExecModal: action,
generatePlan: flow,
handleExecute: flow,
execute: flow,
});
this.selectedExecutionContextState =
this.getInitiallySelectedExecutionContextState();
}
override isChangeExecutionDisabled(): boolean {
return this.multiExecutionKey === '';
}
getInitiallySelectedExecutionContextState(): ServiceExecutionContextState {
return new SingleExecutionContextState(this.execution, this);
}
setMultiExecutionKey(val: string): void {
this.multiExecutionKey = val;
}
changeExecution(): void {
const _execution = new PureMultiExecution(
this.multiExecutionKey,
this.execution.func,
this.serviceEditorState.service,
);
const _parameter = new KeyedExecutionParameter(
`execContext_1`,
this.execution.mapping,
this.execution.runtime,
);
_execution.executionParameters = [_parameter];
service_setExecution(
this.serviceEditorState.service,
_execution,
this.editorStore.changeDetectionState.observerContext,
);
this.serviceEditorState.resetExecutionState();
}
}
export class MultiServicePureExecutionState extends ServicePureExecutionState {
declare execution: PureMultiExecution;
newKeyParameterModal = false;
renameKey: KeyedExecutionParameter | undefined;
singleExecutionKey: KeyedExecutionParameter | undefined;
constructor(
editorStore: EditorStore,
serviceEditorState: ServiceEditorState,
execution: PureMultiExecution,
) {
super(editorStore, serviceEditorState, execution);
makeObservable(this, {
queryState: observable,
selectedExecutionContextState: observable,
runtimeEditorState: observable,
isExecuting: observable,
isGeneratingPlan: observable,
isOpeningQueryEditor: observable,
executionResultText: observable,
executionPlanState: observable,
newKeyParameterModal: observable,
renameKey: observable,
singleExecutionKey: observable,
showChangeExecModal: observable,
setExecutionResultText: action,
closeRuntimeEditor: action,
openRuntimeEditor: action,
useCustomRuntime: action,
setQueryState: action,
autoSelectRuntimeOnMappingChange: action,
updateExecutionQuery: action,
setOpeningQueryEditor: action,
deleteKeyExecutionParameter: action,
setNewKeyParameterModal: action,
changeKeyedExecutionParameter: action,
setRenameKey: action,
addExecutionParameter: action,
setExecutionKey: action,
changeKeyValue: action,
setSingleExecutionKey: action,
setShowChangeExecModal: action,
changeExecution: action,
generatePlan: flow,
execute: flow,
});
this.execution = execution;
this.selectedExecutionContextState =
this.getInitiallySelectedExecutionContextState();
this.queryState = new ServicePureExecutionQueryState(
this.editorStore,
execution,
);
this.executionPlanState = new ExecutionPlanState(
this.editorStore.applicationStore,
this.editorStore.graphManagerState,
);
}
setSingleExecutionKey(val: KeyedExecutionParameter | undefined): void {
this.singleExecutionKey = val;
}
changeExecution(): void {
const mappingExecution = this.singleExecutionKey;
// stub
const _mapping = mappingExecution?.mapping.value ?? stub_Mapping();
const mapping = PackageableElementExplicitReference.create(_mapping);
const runtime = mappingExecution?.runtime ?? stub_PackageableRuntime();
const _execution = new PureSingleExecution(
this.execution.func,
this.serviceEditorState.service,
mapping,
runtime,
);
service_setExecution(
this.serviceEditorState.service,
_execution,
this.editorStore.changeDetectionState.observerContext,
);
this.serviceEditorState.resetExecutionState();
}
setRenameKey(key: KeyedExecutionParameter | undefined): void {
this.renameKey = key;
}
setNewKeyParameterModal(val: boolean): void {
this.newKeyParameterModal = val;
}
setExecutionKey(val: string): void {
pureMultiExecution_setExecutionKey(this.execution, val);
}
getInitiallySelectedExecutionContextState():
| ServiceExecutionContextState
| undefined {
const parameter = this.execution.executionParameters[0];
return parameter
? new KeyedExecutionContextState(parameter, this)
: undefined;
}
changeKeyedExecutionParameter(value: KeyedExecutionParameter): void {
this.selectedExecutionContextState = new KeyedExecutionContextState(
value,
this,
);
}
deleteKeyExecutionParameter(value: KeyedExecutionParameter): void {
pureMultiExecution_deleteExecutionParameter(this.execution, value);
if (value === this.selectedExecutionContextState?.executionContext) {
this.selectedExecutionContextState =
this.getInitiallySelectedExecutionContextState();
}
}
addExecutionParameter(value: string): void {
const _mapping =
this.editorStore.mappingOptions[0]?.value ?? stub_Mapping();
const _key = new KeyedExecutionParameter(
value,
PackageableElementExplicitReference.create(_mapping),
stub_PackageableRuntime(),
);
pureMultiExecution_addExecutionParameter(
this.execution,
_key,
this.editorStore.changeDetectionState.observerContext,
);
this.selectedExecutionContextState = new KeyedExecutionContextState(
_key,
this,
);
}
changeKeyValue(key: KeyedExecutionParameter, value: string): void {
keyedExecutionParameter_setKey(key, value);
}
}