From 5703e55c3f566bfee4f0a2c91f220c32d7e8435e Mon Sep 17 00:00:00 2001
From: Axel RICHARD
+ * Nested index entries are used inside regular {@link IIndexEntry} POJOs to represent elements connected to the elements represented by the {@link IIndexEntry}. In most cases,
+ * {@link INestedIndexEntry} should not contain other {@link INestedIndexEntry}, in order to avoid infinite nesting depth, which cannot be stored in the index. This constraint may be relaxed for
+ * {@link INestedIndexEntry} representing relationships, which can themselves contain an {@link INestedIndexEntry} representing the other end of the relationship.
+ *
+ * For example, an {@link IIndexEntry} representing a SysML element can contain {@link INestedIndexEntry} representing its owned elements, but these nested entries should not contain information
+ * related to their own nested elements. The same {@link IIndexEntry} can contain {@link INestedIndexEntry} representing its relationships, which themselves can contain {@link INestedIndexEntry}
+ * representing the other end of the relationship, but this second level of nested entries cannot contain another level of {@link INestedIndexEntry}.
+ *
+ * The fields {@code id}, {@code type}, and {@code label} should match the identifier of the semantic element, the name of its SysML type, and the label used to present it to the end user. Note that
+ * these fields are serialized with a {@code @} prefix because they represent technical information, and shouldn't clash with potential fields computed from attributes in the SysML metamodel.
+ * An extra field {@code @nestedIndexEntryType} is also produced during the serialization to store the concrete type of the POJO. This lets Sirius Web find the actual type to use when deserializing
+ * query results.
+ * {@link INestedIndexEntry} does not provide {@code iconURLs} nor {@code editingContextId}, because they are not directly manipulated by Sirius Web (their containing {@link IIndexEntry} are).
+ *
+ * {@link IIndexEntry} are top-level POJOs representing SysML elements that are stored in Elasticsearch indices. They can contain {@link INestedIndexEntry} to represent elements connected to the
+ * element they represent.
+ *
+ * SysON serializes SysML elements into entries that can contain nested entries with a maximum depth of:
+ *
+ *
+ *
+ * This record contains relevant data related to a SysML Namespace (e.g. name, short name, qualified name), as well as technical information used by Sirius Web to present index entries to the end user + * (label, icons, type). + *
+ *+ * This class contains {@link INestedIndexEntry}, which are additional objects serialized as sub-fields of this index entry, which allows to access information related to elements connected to the + * namespace (e.g. {@code owner.name} to access the name of the owner, or {@code ownedElement.name} to access the name of the owned elements). + *
+ * + * @param editingContextId the identifier of the editing context containing the namespace + * @param id the identifier of the namespace + * @param type the name of the concrete SysML type of the namespace + * @param label the label of the namespace + * @param iconURLs the URLs of the icons of the namespace + * @param name the name of the namespace + * @param shortName the short name of the namespace + * @param qualifiedName the qualified name of the namespace + * @param owner the nested entry holding information related to the namespace's owner + * @param ownedElement the nested entries holding information related to the namespace owned elements + * + * @author gdaniel + */ +public record NamespaceIndexEntry( + String editingContextId, + String id, + String type, + String label, + List+ * This record does not contain recursive {@link INestedIndexEntry} in order to avoid infinite entry nesting when converting elements into index entries. + * See {@link INestedIndexEntry} for more information. + *
+ * + * @param id the identifier of the element + * @param type the name of the concrete SysML type of the element + * @param label the label of the element + * @param name the name of the element + * @param shortName the short name of the element + * @param qualifiedName the qualified name of the element + * + * @author gdaniel + */ +public record NestedElementIndexEntry( + String id, + String type, + String label, + String name, + String shortName, + String qualifiedName +) implements INestedIndexEntry { +} diff --git a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/NestedIndexEntrySwitch.java b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/NestedIndexEntrySwitch.java new file mode 100644 index 000000000..df8a3bd14 --- /dev/null +++ b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/NestedIndexEntrySwitch.java @@ -0,0 +1,112 @@ +/******************************************************************************* + * Copyright (c) 2026 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.syson.application.index; + +import java.util.Objects; +import java.util.Optional; + +import org.eclipse.emf.ecore.EClass; +import org.eclipse.emf.ecore.EClassifier; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.sirius.components.core.api.IIdentityService; +import org.eclipse.sirius.components.core.api.ILabelService; +import org.eclipse.syson.sysml.Element; +import org.eclipse.syson.sysml.Specialization; +import org.eclipse.syson.sysml.SysmlPackage; +import org.eclipse.syson.sysml.util.SysmlSwitch; + + +/** + * Provides {@link INestedIndexEntry} for SysML elements. + * + *+ * {@link INestedIndexEntry} are used inside {@link org.eclipse.sirius.web.application.index.services.api.IIndexEntry} to represent elements associated to the element represented by their containing + * {@link org.eclipse.sirius.web.application.index.services.api.IIndexEntry}. They are usually not recursive, and may contain different information than their + * {@link org.eclipse.sirius.web.application.index.services.api.IIndexEntry} counterparts. + *
+ * + * @see INestedIndexEntry + * + * @author gdaniel + */ +public class NestedIndexEntrySwitch extends SysmlSwitch+ * This record contains a {@link NestedElementIndexEntry} which allows to access information related to the {@code general} element of the specialization. Note that this nested element is not + * recursive, preventing infinite entry nesting when converting elements into index entries. See {@link INestedIndexEntry} for more information. + *
+ * + * @param id the identifier of the specialization + * @param type the name of the concrete SysML type of the specialization + * @param label the label of the specialization + * @param name the name of the specialization + * @param shortName the short name of the specialization + * @param qualifiedName the qualified name of the specialization + * + * @author gdaniel + */ +public record NestedSpecializationIndexEntry( + String id, + String type, + String label, + String name, + String shortName, + String qualifiedName, + NestedElementIndexEntry general +) implements INestedIndexEntry { +} diff --git a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/SysONIndexCreationService.java b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/SysONIndexCreationService.java new file mode 100644 index 000000000..415ea6e8d --- /dev/null +++ b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/index/SysONIndexCreationService.java @@ -0,0 +1,99 @@ +/******************************************************************************* + * Copyright (c) 2026 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.syson.application.index; + +import java.io.IOException; +import java.util.Objects; +import java.util.Optional; + +import org.eclipse.sirius.web.application.index.services.api.IIndexEntry; +import org.eclipse.sirius.web.application.studio.services.api.IStudioCapableEditingContextPredicate; +import org.eclipse.sirius.web.infrastructure.elasticsearch.services.DefaultIndexCreationService; +import org.eclipse.sirius.web.infrastructure.elasticsearch.services.api.IIndexCreationServiceDelegate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch._types.ElasticsearchException; +import co.elastic.clients.elasticsearch._types.mapping.DynamicTemplate; +import co.elastic.clients.util.NamedValue; + +/** + * Creates indices to store SysML models. + * + *
+ * This service creates and configures the index associated to each editing context. The created index has a name matching the pattern {@code editing-context-
+ * This service converts SysML {@link Element} into {@link IIndexEntry} that can be persisted in the Elasticsearch indices. + *
+ *+ * Note that this service does not accept {@code editingContext} instances that contain Studio data. For these {@code editingContext}, the default implementation of + * {@link org.eclipse.sirius.web.application.index.services.api.IDefaultIndexEntryProvider} is used instead. + *
+ * + * @author gdaniel + */ +@Service +public class SysONIndexEntryProvider implements IIndexEntryProviderDelegate { + + private final IStudioCapableEditingContextPredicate studioCapableEditingContextPredicate; + + private final IIdentityService identityService; + + private final ILabelService labelService; + + public SysONIndexEntryProvider(IStudioCapableEditingContextPredicate studioCapableEditingContextPredicate, IIdentityService identityService, ILabelService labelService) { + this.studioCapableEditingContextPredicate = Objects.requireNonNull(studioCapableEditingContextPredicate); + this.identityService = Objects.requireNonNull(identityService); + this.labelService = Objects.requireNonNull(labelService); + } + + @Override + public boolean canHandle(IEditingContext editingContext, Object object) { + // Only produce SysON index entries for non-studio projects. + return !this.studioCapableEditingContextPredicate.test(editingContext.getId()); + } + + @Override + public Optional+ * This service is called by Sirius Web when changes are detected in an editing context, which may require to update the associated indices. This implementation ensures that all the resources in the + * editing context are indexed, excepted SysML and KerML standard libraries for performance reasons. + *
+ *+ * Note that this service does not accept {@code editingContext} instances that contain Studio data. For these {@code editingContext}, the default implementation of + * {@link org.eclipse.sirius.web.infrastructure.elasticsearch.services.api.IDefaultIndexUpdateService} is used instead. + *
+ * + * + * @author gdaniel + */ +@Service +public class SysONIndexUpdateService implements IIndexUpdateServiceDelegate { + + private final IStudioCapableEditingContextPredicate studioCapableEditingContextPredicate; + + private final IIndexCreationService indexCreationService; + + private final IIndexDeletionService indexDeletionService; + + private final IIndexEntryProvider indexEntryProvider; + + private final Optional+ * This record contains relevant data related to a SysML Type (e.g. name, short name, qualified name), as well as technical information used by Sirius Web to present index entries to the end user + * (label, icons, type). + *
+ *+ * This class contains {@link INestedIndexEntry}, which are additional objects serialized as sub-fields of this index entry, which allows to access information related to elements connected to the + * type (e.g. {@code owner.name} to access the name of the owner, or {@code ownedSpecialization.general.name} to access the name of the element at the other end of an owned specialization). + *
+ * + * @param editingContextId the identifier of the editing context containing the type + * @param id the identifier of the type + * @param type the name of the concrete SysML type of the element + * @param label the label of the type + * @param iconURLs the URLs of the icons of the type + * @param name the name of the type + * @param shortName the short name of the type + * @param qualifiedName the qualified name of the type + * @param owner the nested entry holding information related to the type's owner + * @param ownedSpecialization the nested entries holding information related to the type owned specializations + * @param ownedElement the nested entries holding information related to the type owned elements + * + * @author gdaniel + */ +public record TypeIndexEntry( + String editingContextId, + String id, + String type, + String label, + List+ * Use {@link SysONTestsProperties#ELASTICSEARCH_PROPERTY} in the test configuration to enable Elasticsearch. + *
+ * + * @author gdaniel + */ +@Configuration +@Conditional(OnElasticsearchEnabledTests.class) +public class ElasticsearchDynamicPropertyRegistrar implements DynamicPropertyRegistrar { + + @Override + public void accept(DynamicPropertyRegistry registry) { + registry.add("spring.elasticsearch.uris", AbstractIntegrationTests.ELASTICSEARCH_CONTAINER::getHttpHostAddress); + registry.add("spring.elasticsearch.username", () -> "elastic"); + registry.add("spring.elasticsearch.password", () -> ElasticsearchContainer.ELASTICSEARCH_DEFAULT_PASSWORD); + } +} diff --git a/backend/application/syson-application/src/test/java/org/eclipse/syson/OnElasticsearchEnabledTests.java b/backend/application/syson-application/src/test/java/org/eclipse/syson/OnElasticsearchEnabledTests.java new file mode 100644 index 000000000..af80951da --- /dev/null +++ b/backend/application/syson-application/src/test/java/org/eclipse/syson/OnElasticsearchEnabledTests.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) 2026 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.syson; + +import java.util.Arrays; + +import org.springframework.boot.autoconfigure.condition.ConditionMessage; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; + +/** + * Custom condition used to detect that a test requires Elasticsearch. + * + * @author gdaniel + */ +public class OnElasticsearchEnabledTests extends SpringBootCondition { + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + var message = ConditionMessage.forCondition(this.getClass().getSimpleName()).notAvailable(SysONTestsProperties.SYSON_TEST_ENABLED); + ConditionOutcome outcome = ConditionOutcome.noMatch(message); + + var sysonTestEnabled = context.getEnvironment().getProperty(SysONTestsProperties.SYSON_TEST_ENABLED, ""); + var sysonTestEnabledFeatures = Arrays.stream(sysonTestEnabled.split(",")).map(String::trim).toList(); + + if (sysonTestEnabledFeatures.contains(SysONTestsProperties.ELASTICSEARCH)) { + message = ConditionMessage.forCondition(this.getClass().getSimpleName()).available(SysONTestsProperties.ELASTICSEARCH_PROPERTY); + outcome = ConditionOutcome.match(message); + } + + return outcome; + } + +} diff --git a/backend/application/syson-application/src/test/java/org/eclipse/syson/SysONTestsProperties.java b/backend/application/syson-application/src/test/java/org/eclipse/syson/SysONTestsProperties.java index fc7236f1d..f7897955e 100644 --- a/backend/application/syson-application/src/test/java/org/eclipse/syson/SysONTestsProperties.java +++ b/backend/application/syson-application/src/test/java/org/eclipse/syson/SysONTestsProperties.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2025 Obeo. + * Copyright (c) 2025, 2026 Obeo. * This program and the accompanying materials * are made available under the terms of the Eclipse Public License v2.0 * which accompanies this distribution, and is available at @@ -25,4 +25,8 @@ public class SysONTestsProperties { public static final String NO_DEFAULT_LIBRARIES_PROPERTY = SYSON_TEST_ENABLED + "=" + NO_DEFAULT_LIBRARIES; + public static final String ELASTICSEARCH = "elastic-search"; + + public static final String ELASTICSEARCH_PROPERTY = SYSON_TEST_ENABLED + "=" + ELASTICSEARCH; + } diff --git a/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/omnibox/ProjectsOmniboxControllerIntegrationTests.java b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/omnibox/ProjectsOmniboxControllerIntegrationTests.java new file mode 100644 index 000000000..d52d7ac5d --- /dev/null +++ b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/omnibox/ProjectsOmniboxControllerIntegrationTests.java @@ -0,0 +1,108 @@ +/******************************************************************************* + * Copyright (c) 2026 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.syson.application.controllers.omnibox; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import com.jayway.jsonpath.JsonPath; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.eclipse.sirius.components.core.api.IEditingContextSearchService; +import org.eclipse.sirius.web.infrastructure.elasticsearch.services.api.IIndexUpdateService; +import org.eclipse.sirius.web.tests.graphql.ProjectsOmniboxSearchQueryRunner; +import org.eclipse.syson.AbstractIntegrationTests; +import org.eclipse.syson.SysONTestsProperties; +import org.eclipse.syson.application.data.SimpleProjectElementsTestProjectData; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.transaction.annotation.Transactional; + +import co.elastic.clients.elasticsearch.ElasticsearchClient; + +/** + * Integration tests of the projects omnibox controllers. + * + * @author gdaniel + */ +@Transactional +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { SysONTestsProperties.SYSON_TEST_ENABLED + "=" + SysONTestsProperties.NO_DEFAULT_LIBRARIES + ", " + SysONTestsProperties.ELASTICSEARCH }) +public class ProjectsOmniboxControllerIntegrationTests extends AbstractIntegrationTests { + + @Autowired + private ProjectsOmniboxSearchQueryRunner projectsOmniboxSearchQueryRunner; + + @Autowired + private IIndexUpdateService indexUpdateService; + + @Autowired + private OptionalxqcU*w~N2 zs<6Sv7!3WoFJh2w!GSLFaphg#cP-v1c;u>) zI#~V=;3nfw=+ULIpBhAsIuP7?#y*u cnQoyBC94apy3uo6? zd1ZV3P~GB_%tw?p(Y uRO(AS&;YDuog>2K? =0xf*&=o5UXbo}*0(|ZNu1xQU{_FkU;MMyaUcBr z$2tk_s%1TGq=}tMxiV9@oFJN-&d3DN+J 2@x!O1m~60o|LD zgJ*;mm+VDe7(D(nk$(yW{$rOm^4He&$R-$~kWVp%ZN`%LkMz-tm7LM^=!lpVxUZwA z?(ADU@j|wt-A7aYA-ZJwMnI;FE;uqJ3smKf$X^q%(q7o5ed%(0@ZN0oqNEC}Bgclz zL$HfgyV7WAzRoVdBup}1Ja+a)X!c!2W`1uTLoD`tsla@wNCABa9po@Xk$0Aq-^?iX zL|6I7GM#=aObRZ``8z(Va*iVU@IfD`MV^At!uG4(X3?W8qY4IoWg(n-+G0vBa8k+G zob#J;X8-BeMq~m?NiT71wv>`Q4 &Q>>{H^Los8DGzLAlz<(WUsi l^Haeb^ccGhPtGl>P07FvE456gMGoHcCp zx5>RUHPo$XMqo9v9^GGFMwP3v5SnttY2@Tu=3&=4s>&hhoXYB6{!xfwKM*OM-}+Ei z-z>EjT(`SV-evonxGcz84z5x`*2U+bSYiFiL4lR0c2X< _^H$LDO+ mBD} ObV zNcZ~{b7TYC>$v#Iji#SdRAIt(L*Ae~uA#Bki@0?nC-;wR!5NsaNtU|sAC(Ou@Lf3{ z>`JzzjRc{KR0w%+%QPiQ+0*cgbVA}0eX;gk4lf}J&(%6jw;uIJDV071i=@=U5Nn5$ zozE799|nZu7FfQ8qu7Neh0P1^3a!3J_BPMK?42Z@?}bziB!iZxD-$fOmgV)irN?kX z63?GD=>jE|BX1SlO t6@UO>cHB}OJpfFs)5o*;uYAT$@Ayjd8QPP zxr?EU42>YPY~cy(^LE}`7wr4`mdW8tFYoSouO#CwbO$Y6*}+3u8deY2_KGB!cXio( zQmg{2^&hkF=BRZ~`CGy#XLa0+9}2;`)u;DKC_XP&axTikjpjT10B?lSWD~#1v#hCm zy%Su@f`LMKe4DVpzUQ6M;1)0BJg{JcoE7vOrr6TWV6Dcx_!@5_IP+}z7(Hszio3B= z1>|{P@CI{69@NeJXBSagfFunr?`grxslox`XR@irMkus~!%^LGlDY4TygT^Eq@Jr$ zAQn;i;3;Lk!Hdn&qT^ln(>KFo8KDfy`a>vtNajRW4LHL30wp`)yJ9N2oDaKiy@4om ziSFZ4LD0VMFE &Arx~sUAu6sTVEobE?_9%iWmJ1~2Tirt92)(nkh!Yi$lHClx0m{zn zv(LyF@5?tRr)GoT30S4FyQgE>?l8 n14qb7B{WDiXUT+A)<@)Z-_d5gZ%kyS@C zygB5Z9F7)Uu0)ZI&(I11x{^)^z0 F=?>q@ X8{M4^XPon +cNlJO@_|Ccs2oZ!m#RhzL)8DOg zdfqZ4(=VLAK}1_0+5|I$dYpT|GXW#&Dk{>v2-Wv%y*TnHV>Zgm!sX+u{0fsJ{2uXL z=sZ)+7$x0yF5#Y@m;;vfX+v(PBPSPf&rpZMO|R0Oadpo;gTC9uzffRA9 F z2DT-Xd@vCYkBfE_+u|dN{oMOOAz{G>$coXaiPP--l &GcYT1V_M<>bM1zHwSJ_r}1wFc~Rlzj!&~T5a zkx4*x)^o!Chynf0pKtXM?Cxs{p>m5N1&{ciyfRP|d0FcQW#NYJhb*l1TFfexZ+E>U zr5A4&uX;3pEdRdNp3Ubx;Rw^f5J6(4iEwwf){<7~{?NYY+ugtpu0^|~a25H4bE7d9 zPd6y}gOjx_$)55b3qE9+5v~uQgQ=zGRIc$P-R8PGH1g`3B790CcF>Kv0vWB~ZG4-? zaS~r;f|c^wI #IOn2Xm22E-5 zyT2} NnUd5JUE__d{!Jiy|egz0rMcS#g^t;4{CxyLTkU zwMqFa*o6A(G4ZeW7|=X@_TUlOEK--RaRn|zFtVX#ef?byztA*e)7`J0)1_nqEwGIj zGVnZtY=W8i$a=$x`NskbX!p_mOl1Kr!51+yYwxV$R+)$@nPlKBY|~uaNRApv8|H^F zCuNtztb~GG9F!01YWYxJS?`;4^7Zkj3t}nKMkTUSG-eZV$3wxPUUJ~q$K{u!8KT@@ zCG3{ah)NhY^_j-FTUD~FWvb3aPwS6$lkD>XKpFe$@uTc7(&F^vSLYLRT#R$}T;2f? z)Gt+Xs-NF#T3;;ly1OB3- !aI9uM{+#6Q_@~JAI+6to7k<8F~9LGPm{X zFM|i-0IV}u!Ow_(6oj@QlbaY$W>it({x&i9eEz<-%{Ne)pT@-faH~=Vfv*Z$&+ag) zIDH}w!-cj`iGgu6Q}~No_6)4kC1QDuIrns{#`KFGr4ySKSZvY;JJg7x1V(sXFL%7D z4A27S`yvW?R$gr&U#rgrDXX%-Nnu+00rNO!E>v_#NH5AVn9y?c_1%(N0BM5pI*w!y zL7n?j7I$*B2Gv$D@$ITx3VUK_Barzju@Jn^0^jTKbt nWq_%y|)f z$=`7!78%fM3JkU`G0cBZVKNfl;*kW yN8)B4{H`uP2D-82=11pj=qeN 8mNF_6HsLsQVRKCa;db?zWdZ#v|3*~Bou$t4t0`N@sRp_n$J6QyFD-)BEQ zo~4GuiRMdFv&L2Uc792RPH(DQ$t`-6njA@Dzb+u*E*o7Fo&ef&TwC$e9g?8*@^&j6 zMWE?8Oz6;T(}4JHLK(?R&ormw_4CmO=>A@b&DV1!X%%7t_*daqZB`!=vB{!q^p>g6 zM%Fr4C$iKmCV%D!B?^gXXqXfoX`uHUc?IP&@i?6bry^dDdO_n?P6UF>Bp5lHTi}*t z_o>gFl<+9izWSmpMUGuPPv^6Xfsek&xl6!O*bIN-$Z2z@`$ODv_vqYU|4l3T@6ro0 zPTCF;p_vKfSn0Go{)`L-F7?F?GZsP@Nfykka$_7Ye{NYMS0Y}sF%Fz)xsMA`^*z?Y z;$xYYN4fkUR0S-~GxF6%a^y6&GMN8}OHnp~_N`+Zs9?2pU+-*j?D~XDN@;4fI@UrN zaHy)PL~NAc5SQb2>3;k}E)#%T^0DcBXCFf0kzoHX5Nyx85%9Bm@b`#OU>62@;&b3B zs`t(QKXW!78$U^%k-aC_)(>zhf#R8H4aeio;rqI|0nIY+gX>+oEeJSA1q5E-agGP6 z(vsJF8diGZFvY((J} d##Io&ZpdZ%ktrV7YjGeF=YIo`kKJ%?cvTFeb0RM!1>G8G2LX;q2Y1zf9G zm%u!bZ0uWdNCL%y?e;cq(B4G ^6If_rQ_b!>q_p)F+%ImkC@jX79^Spiuuw%b`%@mS`b;c;C4kC% z;t-h{q?eo+KlDRuiH399_N}FL$PpWa__K;WLsHo*HOjhq4&D5BUY#`*6Jxjg3r) ckc Ywuqb7`zgAQECEV3gKhdlpp&}FQGZ4#P*r*9Z_fa z?OyIDYFRff91Koq47_`uv<5jAUNaG6-k;3OuQc?58f00y*({@wYI5rJ>iW=4X`;`s z#s( HFGscdA&uqNAIBOfJST6d{-%3JmCJ$1y@SUGtGovZszS zgVxsCkrCQ^m^OktnI$$}pL^eP>q4kxo>&XXt2@M)#^*-*M6+>1;Qqt;T6L@p#1CX2 z5C|Qa=UU>z+mVGQ2iO7w-+)uHh;^3m7hu4=FBdm 53qHqMOme%#1)v=EaRTF@chyH6STzSx!4gbbEudK0lrC-M0A{&%ez3v z ;vT+RK)Vr*}JIG{n;THHuYS%%N<2q z7tc*tUxTn?(hdj2MVzzNv=LI{2}C=mC?r=$vqfC}ZZ%K@Gl;*{0Ju2>I`XR^b#PA% z(}RHr6^1T3VIRsId4EBHTeu$=m~7SHz^qm)(^q4J`iW;57q=Chqb(2@V;u`{{q En#$qDKo+L4%5q>iBNr=T>>OBu?=3qJO>neFEOMbRcewd+`AX2a&tzc7JY^4<~ z2VZ6D_Cgds?5`SO4BC6eRY&k_q3!cJ@Bu;Mn3lkgrl?5zY07uYNy5U*g}9294S$&t z->3ZlVu8)l6+ls(9xkyRZv=E=F~at{?{~(R2UiPhsfERb@`kz;1p1Myg;UMg`R8t3 z`@o6(K)=c|S>^5CbCmttqSCaj57s-$(zy5eh?4Qt?7p_LW!?7?&s{kspDC1iTuxUn zl}=+-=L!3E&Kk9Nm=#Rx!~olQ@SK!AYo%FORGqtU%R3>PA9LLHQsMSPZse(^9dD&H zII2R6M2r9^!2^WHOJyY0t$(@DR;UH`>$>wbcuD3*N}S1 f|6C}J(%abCT^ABCdUQ$LH`;!EuADRqU z2UpIM=^faa5P7VA2zURv(5CLK=1Xc_2&}}BD47EMBTMtVB5-+2t?B I+=$j|nx`&ygy5HoL zAqOm(P2UzaG<=goD!&a6PV+s=s@4`zYoDu75Ehpo?1(%@J3DAU8W1D;3X}Cf`vKbD z7#giBDriW*;KPvhL|RTXx|{PtS5EGVvgvDOJP9X3gV_b#+M~Guz^$3pAq25M$!zi> z0;Fn*nmXk*sdZUVyxkVSP$*t%yv+!NEIY`8cg~oLOQ>Nx8{5?Cz_iQ_makTM`Ygk3 zDtyPB?pG~#fXDYKzrP8 `mmg`2Qfm5zRoE>HKD`cYRg&SdJVSdwP(hSrl&z}YAVzX&ZVeuampx$Gaf^_ zQBeLLh@V^*Z7m%B>`^MipZ)0#H@{EIHMC?5YXmJc-4 7Y%