From 70f1ea0a9d0224312a3896df94389ae387995fb2 Mon Sep 17 00:00:00 2001
From: Christian Sailer (val configuration: NodeConfiguration,
val serverThread: AffinityExecutor.ServiceAffinityExecutor,
val busyNodeLatch: ReusableLatch = ReusableLatch(),
djvmBootstrapSource: ApiSource = EmptyApi,
- djvmCordaSource: UserSource? = null) : SingletonSerializeAsToken() {
+ djvmCordaSource: UserSource? = null,
+ protected val allowHibernateToManageAppSchema: Boolean = false) : SingletonSerializeAsToken() {
protected abstract val log: Logger
@Suppress("LeakingThis")
@@ -222,6 +223,11 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
protected val runOnStop = ArrayList<() -> Any?>()
+ protected open val runMigrationScripts: Boolean = configuredDbIsInMemory()
+
+ // if the configured DB is in memory, we will need to run db migrations, as the db does not persist between runs.
+ private fun configuredDbIsInMemory() = configuration.dataSourceProperties.getProperty("dataSource.url").startsWith("jdbc:h2:mem:")
+
init {
(serverThread as? ExecutorService)?.let {
runOnStop += {
@@ -233,6 +239,12 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
}
quasarExcludePackages(configuration)
+
+ if (allowHibernateToManageAppSchema && !configuration.devMode) {
+ throw ConfigurationException("Hibernate can only be used to manage app schema in development while using dev mode. " +
+ "Please remove the --allow-hibernate-to-manage-app-schema command line flag and provide schema migration scripts for your CorDapps."
+ )
+ }
}
private val notaryLoader = configuration.notary?.let {
@@ -248,7 +260,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
schemaService,
configuration.dataSourceProperties,
cacheFactory,
- cordappLoader.appClassLoader)
+ cordappLoader.appClassLoader,
+ allowHibernateToManageAppSchema)
private val transactionSupport = CordaTransactionSupportImpl(database)
@@ -458,6 +471,33 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
}
}
+ open fun runDatabaseMigrationScripts() {
+ check(started == null) { "Node has already been started" }
+ Node.printBasicNodeInfo("Running database schema migration scripts ...")
+ val props = configuration.dataSourceProperties
+ if (props.isEmpty) throw DatabaseConfigurationException("There must be a database configured.")
+ database.startHikariPool(props, schemaService.internalSchemas(), metricRegistry, this.cordappLoader, configuration.baseDirectory, configuration.myLegalName, runMigrationScripts = true)
+ // Now log the vendor string as this will also cause a connection to be tested eagerly.
+ logVendorString(database, log)
+ if (allowHibernateToManageAppSchema) {
+ Node.printBasicNodeInfo("Initialising CorDapps to get schemas created by hibernate")
+ val trustRoot = initKeyStores()
+ networkMapClient?.start(trustRoot)
+ val (netParams, signedNetParams) = NetworkParametersReader(trustRoot, networkMapClient, configuration.baseDirectory).read()
+ log.info("Loaded network parameters: $netParams")
+ check(netParams.minimumPlatformVersion <= versionInfo.platformVersion) {
+ "Node's platform version is lower than network's required minimumPlatformVersion"
+ }
+ networkMapCache.start(netParams.notaries)
+
+ database.transaction {
+ networkParametersStorage.setCurrentParameters(signedNetParams, trustRoot)
+ cordappProvider.start()
+ }
+ }
+ Node.printBasicNodeInfo("Database migration done.")
+ }
+
open fun start(): S {
check(started == null) { "Node has already been started" }
@@ -946,7 +986,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
protected open fun startDatabase() {
val props = configuration.dataSourceProperties
if (props.isEmpty) throw DatabaseConfigurationException("There must be a database configured.")
- database.startHikariPool(props, configuration.database, schemaService.internalSchemas(), metricRegistry, this.cordappLoader, configuration.baseDirectory, configuration.myLegalName)
+ database.startHikariPool(props, schemaService.internalSchemas(), metricRegistry, this.cordappLoader, configuration.baseDirectory, configuration.myLegalName, runMigrationScripts = runMigrationScripts)
// Now log the vendor string as this will also cause a connection to be tested eagerly.
logVendorString(database, log)
}
@@ -1313,13 +1353,15 @@ class FlowStarterImpl(private val smm: StateMachineManager, private val flowLogi
class ConfigurationException(message: String) : CordaException(message)
+@Suppress("LongParameterList")
fun createCordaPersistence(databaseConfig: DatabaseConfig,
wellKnownPartyFromX500Name: (CordaX500Name) -> Party?,
wellKnownPartyFromAnonymous: (AbstractParty) -> Party?,
schemaService: SchemaService,
hikariProperties: Properties,
cacheFactory: NamedCacheFactory,
- customClassLoader: ClassLoader?): CordaPersistence {
+ customClassLoader: ClassLoader?,
+ allowHibernateToManageAppSchema: Boolean = false): CordaPersistence {
// Register the AbstractPartyDescriptor so Hibernate doesn't warn when encountering AbstractParty. Unfortunately
// Hibernate warns about not being able to find a descriptor if we don't provide one, but won't use it by default
// so we end up providing both descriptor and converter. We should re-examine this in later versions to see if
@@ -1330,25 +1372,38 @@ fun createCordaPersistence(databaseConfig: DatabaseConfig,
val jdbcUrl = hikariProperties.getProperty("dataSource.url", "")
return CordaPersistence(
- databaseConfig,
- schemaService.schemas,
- jdbcUrl,
- cacheFactory,
- attributeConverters, customClassLoader,
- errorHandler = { e ->
- // "corrupting" a DatabaseTransaction only inside a flow state machine execution
- FlowStateMachineImpl.currentStateMachine()?.let {
- // register only the very first exception thrown throughout a chain of logical transactions
- setException(e)
- }
- })
+ databaseConfig,
+ schemaService.schemas,
+ jdbcUrl,
+ cacheFactory,
+ attributeConverters, customClassLoader,
+ errorHandler = { e ->
+ // "corrupting" a DatabaseTransaction only inside a flow state machine execution
+ FlowStateMachineImpl.currentStateMachine()?.let {
+ // register only the very first exception thrown throughout a chain of logical transactions
+ setException(e)
+ }
+ },
+ allowHibernateToManageAppSchema = allowHibernateToManageAppSchema)
}
-fun CordaPersistence.startHikariPool(hikariProperties: Properties, databaseConfig: DatabaseConfig, schemas: Set> {
+ private fun startNotaryIdentityGeneration(): CordaFuture
>> {
return executorService.fork {
notarySpecs.map { spec ->
+ val notaryConfig = mapOf("notary" to mapOf("validating" to spec.validating))
+ val parameters = NodeParameters(rpcUsers = spec.rpcUsers, verifierType = spec.verifierType, customOverrides = notaryConfig + notaryCustomOverrides, maximumHeapSize = spec.maximumHeapSize)
+ val config = createConfig(spec.name, parameters)
val identity = when (spec.cluster) {
null -> {
DevIdentityGenerator.installKeyStoreWithNodeIdentity(baseDirectory(spec.name), spec.name)
@@ -499,14 +512,14 @@ class DriverDSLImpl(
}
else -> throw UnsupportedOperationException("Cluster spec ${spec.cluster} not supported by Driver")
}
- NotaryInfo(identity, spec.validating)
+ Pair(config, NotaryInfo(identity, spec.validating))
}
}
}
private fun startAllNotaryRegistrations(
rootCert: X509Certificate,
- compatibilityZone: CompatibilityZoneParams): CordaFuture
> {
+ compatibilityZone: CompatibilityZoneParams): CordaFuture
>> {
// Start the registration process for all the notaries together then wait for their responses.
return notarySpecs.map { spec ->
require(spec.cluster == null) { "Registering distributed notaries not supported" }
@@ -518,51 +531,56 @@ class DriverDSLImpl(
spec: NotarySpec,
rootCert: X509Certificate,
compatibilityZone: CompatibilityZoneParams
- ): CordaFuture
> {
+ private fun startSingleNotary(config: NodeConfig, spec: NotarySpec, localNetworkMap: LocalNetworkMap?, customOverrides: Map
> {
val notaryConfig = mapOf("notary" to mapOf("validating" to spec.validating))
return startRegisteredNode(
- spec.name,
+ config,
localNetworkMap,
NodeParameters(rpcUsers = spec.rpcUsers, verifierType = spec.verifierType, customOverrides = notaryConfig + customOverrides, maximumHeapSize = spec.maximumHeapSize)
).map { listOf(it) }
@@ -585,20 +603,26 @@ class DriverDSLImpl(
val nodeNames = generateNodeNames(spec)
val clusterAddress = portAllocation.nextHostAndPort()
+ val firstParams = NodeParameters(rpcUsers = spec.rpcUsers, verifierType = spec.verifierType, customOverrides = notaryConfig(clusterAddress))
+ val firstConfig = createSchema(createConfig(nodeNames[0], firstParams), allowHibernateToManageAppSchema)
+
// Start the first node that will bootstrap the cluster
val firstNodeFuture = startRegisteredNode(
- nodeNames[0],
+ firstConfig.getOrThrow(),
localNetworkMap,
- NodeParameters(rpcUsers = spec.rpcUsers, verifierType = spec.verifierType, customOverrides = notaryConfig(clusterAddress))
+ firstParams
)
// All other nodes will join the cluster
val restNodeFutures = nodeNames.drop(1).map {
val nodeAddress = portAllocation.nextHostAndPort()
+ val params = NodeParameters(rpcUsers = spec.rpcUsers, verifierType = spec.verifierType, customOverrides = notaryConfig(nodeAddress, clusterAddress))
+ val config = createSchema(createConfig(it, params), allowHibernateToManageAppSchema)
startRegisteredNode(
- it,
+ config.getOrThrow(),
localNetworkMap,
- NodeParameters(rpcUsers = spec.rpcUsers, verifierType = spec.verifierType, customOverrides = notaryConfig(nodeAddress, clusterAddress))
+ params
+
)
}
@@ -663,7 +687,7 @@ class DriverDSLImpl(
)
val nodeFuture = if (parameters.startInSameProcess ?: startNodesInProcess) {
- val nodeAndThreadFuture = startInProcessNode(executorService, config)
+ val nodeAndThreadFuture = startInProcessNode(executorService, config, allowHibernateToManageAppSchema)
shutdownManager.registerShutdown(
nodeAndThreadFuture.map { (node, thread) ->
{
@@ -689,6 +713,9 @@ class DriverDSLImpl(
nodeFuture
} else {
val debugPort = if (isDebug) debugPortAllocation.nextPort() else null
+ log.info("StartNodeInternal for ${config.corda.myLegalName.organisation} - calling create schema")
+ createSchema(config, allowHibernateToManageAppSchema).getOrThrow()
+ log.info("StartNodeInternal for ${config.corda.myLegalName.organisation} - create schema done")
val process = startOutOfProcessNode(
config,
quasarJarPath,
@@ -699,7 +726,10 @@ class DriverDSLImpl(
parameters.maximumHeapSize,
parameters.logLevelOverride,
identifier,
- environmentVariables
+ environmentVariables,
+ extraCmdLineFlag = listOfNotNull(
+ if (allowHibernateToManageAppSchema) "--allow-hibernate-to-manage-app-schema" else null
+ ).toTypedArray()
)
// Destroy the child process when the parent exits.This is needed even when `waitForAllNodesToFinish` is
@@ -853,7 +883,8 @@ class DriverDSLImpl(
private fun startInProcessNode(
executorService: ScheduledExecutorService,
- config: NodeConfig
+ config: NodeConfig,
+ allowHibernateToManageAppSchema: Boolean
): CordaFuture
(val configuration: NodeConfiguration,
val busyNodeLatch: ReusableLatch = ReusableLatch(),
djvmBootstrapSource: ApiSource = EmptyApi,
djvmCordaSource: UserSource? = null,
- protected val allowHibernateToManageAppSchema: Boolean = false) : SingletonSerializeAsToken() {
+ protected val allowHibernateToManageAppSchema: Boolean = false,
+ private val allowAppSchemaUpgradeWithCheckpoints: Boolean = false) : SingletonSerializeAsToken() {
protected abstract val log: Logger
+
@Suppress("LeakingThis")
private var tokenizableServices: MutableList(val configuration: NodeConfiguration,
}
}
- open fun runDatabaseMigrationScripts() {
+ open fun runDatabaseMigrationScripts(
+ updateCoreSchemas: Boolean,
+ updateAppSchemas: Boolean,
+ updateAppSchemasWithCheckpoints: Boolean
+ ) {
check(started == null) { "Node has already been started" }
Node.printBasicNodeInfo("Running database schema migration scripts ...")
val props = configuration.dataSourceProperties
if (props.isEmpty) throw DatabaseConfigurationException("There must be a database configured.")
- database.startHikariPool(props, schemaService.internalSchemas(), metricRegistry, this.cordappLoader, configuration.baseDirectory, configuration.myLegalName, runMigrationScripts = true)
+ database.startHikariPool(props, metricRegistry) { dataSource, haveCheckpoints ->
+ SchemaMigration(dataSource, cordappLoader, configuration.baseDirectory, configuration.myLegalName)
+ .checkOrUpdate(schemaService.internalSchemas, updateCoreSchemas, haveCheckpoints, true)
+ .checkOrUpdate(schemaService.appSchemas, updateAppSchemas, !updateAppSchemasWithCheckpoints && haveCheckpoints, false)
+ }
// Now log the vendor string as this will also cause a connection to be tested eagerly.
logVendorString(database, log)
if (allowHibernateToManageAppSchema) {
@@ -987,7 +1023,12 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
protected open fun startDatabase() {
val props = configuration.dataSourceProperties
if (props.isEmpty) throw DatabaseConfigurationException("There must be a database configured.")
- database.startHikariPool(props, schemaService.internalSchemas(), metricRegistry, this.cordappLoader, configuration.baseDirectory, configuration.myLegalName, runMigrationScripts = runMigrationScripts)
+ database.startHikariPool(props, metricRegistry) { dataSource, haveCheckpoints ->
+ SchemaMigration(dataSource, cordappLoader, configuration.baseDirectory, configuration.myLegalName)
+ .checkOrUpdate(schemaService.internalSchemas, runMigrationScripts, haveCheckpoints, true)
+ .checkOrUpdate(schemaService.appSchemas, runMigrationScripts, haveCheckpoints && !allowAppSchemaUpgradeWithCheckpoints, false)
+ }
+
// Now log the vendor string as this will also cause a connection to be tested eagerly.
logVendorString(database, log)
}
@@ -1388,23 +1429,16 @@ fun createCordaPersistence(databaseConfig: DatabaseConfig,
allowHibernateToManageAppSchema = allowHibernateToManageAppSchema)
}
-@Suppress("LongParameterList", "ComplexMethod", "ThrowsCount")
+@Suppress("ThrowsCount")
fun CordaPersistence.startHikariPool(
hikariProperties: Properties,
- schemas: Set(val configuration: NodeConfiguration,
Node.printBasicNodeInfo("Database migration done.")
}
+ fun runSchemaSync() {
+ check(started == null) { "Node has already been started" }
+ Node.printBasicNodeInfo("Synchronising CorDapp schemas to the changelog ...")
+ val hikariProperties = configuration.dataSourceProperties
+ if (hikariProperties.isEmpty) throw DatabaseConfigurationException("There must be a database configured.")
+
+ val dataSource = DataSourceFactory.createDataSource(hikariProperties, metricRegistry = metricRegistry)
+ SchemaMigration(dataSource, cordappLoader, configuration.baseDirectory, configuration.myLegalName)
+ .synchroniseSchemas(schemaService.appSchemas, false)
+ Node.printBasicNodeInfo("CorDapp schemas synchronised")
+ }
+
open fun start(): S {
check(started == null) { "Node has already been started" }
@@ -1414,7 +1426,7 @@ fun createCordaPersistence(databaseConfig: DatabaseConfig,
val jdbcUrl = hikariProperties.getProperty("dataSource.url", "")
return CordaPersistence(
- databaseConfig,
+ databaseConfig.exportHibernateJMXStatistics,
schemaService.schemas,
jdbcUrl,
cacheFactory,
diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt
index f2ae464f00..1940422fad 100644
--- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt
+++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt
@@ -77,6 +77,7 @@ open class NodeStartupCli : CordaCliWrapper("corda", "Runs a Corda Node") {
private val initialRegistrationCli by lazy { InitialRegistrationCli(startup) }
private val validateConfigurationCli by lazy { ValidateConfigurationCli() }
private val runMigrationScriptsCli by lazy { RunMigrationScriptsCli(startup) }
+ private val synchroniseAppSchemasCli by lazy { SynchroniseSchemasCli(startup) }
override fun initLogging(): Boolean = this.initLogging(cmdLineOptions.baseDirectory)
@@ -85,7 +86,8 @@ open class NodeStartupCli : CordaCliWrapper("corda", "Runs a Corda Node") {
justGenerateRpcSslCertsCli,
initialRegistrationCli,
validateConfigurationCli,
- runMigrationScriptsCli)
+ runMigrationScriptsCli,
+ synchroniseAppSchemasCli)
override fun call(): Int {
if (!validateBaseDirectory()) {
diff --git a/node/src/main/kotlin/net/corda/node/internal/subcommands/SynchroniseSchemasCli.kt b/node/src/main/kotlin/net/corda/node/internal/subcommands/SynchroniseSchemasCli.kt
new file mode 100644
index 0000000000..aa81d9cd5c
--- /dev/null
+++ b/node/src/main/kotlin/net/corda/node/internal/subcommands/SynchroniseSchemasCli.kt
@@ -0,0 +1,16 @@
+package net.corda.node.internal.subcommands
+
+import net.corda.node.internal.Node
+import net.corda.node.internal.NodeCliCommand
+import net.corda.node.internal.NodeStartup
+import net.corda.node.internal.RunAfterNodeInitialisation
+
+class SynchroniseSchemasCli(startup: NodeStartup) : NodeCliCommand("sync-app-schemas", "Create changelog entries for liquibase files found in CorDapps", startup) {
+ override fun runProgram(): Int {
+ return startup.initialiseAndRun(cmdLineOptions, object : RunAfterNodeInitialisation {
+ override fun run(node: Node) {
+ node.runSchemaSync()
+ }
+ })
+ }
+}
\ No newline at end of file
diff --git a/node/src/main/kotlin/net/corda/node/migration/CordaMigration.kt b/node/src/main/kotlin/net/corda/node/migration/CordaMigration.kt
index 79d2910e7e..0d0832c7bd 100644
--- a/node/src/main/kotlin/net/corda/node/migration/CordaMigration.kt
+++ b/node/src/main/kotlin/net/corda/node/migration/CordaMigration.kt
@@ -10,9 +10,11 @@ import net.corda.core.identity.CordaX500Name
import net.corda.core.schemas.MappedSchema
import net.corda.node.SimpleClock
import net.corda.node.services.identity.PersistentIdentityService
-import net.corda.node.services.persistence.*
+import net.corda.node.services.persistence.AbstractPartyToX500NameAsStringConverter
+import net.corda.node.services.persistence.DBTransactionStorage
+import net.corda.node.services.persistence.NodeAttachmentService
+import net.corda.node.services.persistence.PublicKeyToTextConverter
import net.corda.nodeapi.internal.persistence.CordaPersistence
-import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.nodeapi.internal.persistence.SchemaMigration.Companion.NODE_X500_NAME
import java.io.PrintWriter
import java.sql.Connection
@@ -74,7 +76,6 @@ abstract class CordaMigration : CustomTaskChange {
cacheFactory: MigrationNamedCacheFactory,
identityService: PersistentIdentityService,
schema: Set(val configuration: NodeConfiguration,
}
}
+ @Suppress("ComplexMethod")
open fun start(): S {
check(started == null) { "Node has already been started" }
@@ -487,7 +490,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
startShell()
networkMapClient?.start(trustRoot)
- val (netParams, signedNetParams) = NetworkParametersReader(trustRoot, networkMapClient, configuration.baseDirectory).read()
+ val networkParametersReader = NetworkParametersReader(trustRoot, networkMapClient, configuration.baseDirectory)
+ val (netParams, signedNetParams) = networkParametersReader.read()
log.info("Loaded network parameters: $netParams")
check(netParams.minimumPlatformVersion <= versionInfo.platformVersion) {
"Node's platform version is lower than network's required minimumPlatformVersion"
@@ -508,13 +512,27 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
val (nodeInfo, signedNodeInfo) = nodeInfoAndSigned
identityService.ourNames = nodeInfo.legalIdentities.map { it.name }.toSet()
services.start(nodeInfo, netParams)
+
+ val networkParametersHotloader = if (networkMapClient == null) {
+ null
+ } else {
+ NetworkParametersHotloader(networkMapClient, trustRoot, netParams, networkParametersReader, networkParametersStorage).also {
+ it.addNotaryUpdateListener(networkMapCache)
+ it.addNotaryUpdateListener(identityService)
+ it.addNetworkParametersChangedListeners(services)
+ it.addNetworkParametersChangedListeners(networkMapUpdater)
+ }
+ }
+
networkMapUpdater.start(
trustRoot,
signedNetParams.raw.hash,
signedNodeInfo,
netParams,
keyManagementService,
- configuration.networkParameterAcceptanceSettings!!)
+ configuration.networkParameterAcceptanceSettings!!,
+ networkParametersHotloader)
+
try {
startMessagingService(rpcOps, nodeInfo, myNotaryIdentity, netParams)
} catch (e: Exception) {
@@ -1158,7 +1176,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
}
}
- inner class ServiceHubInternalImpl : SingletonSerializeAsToken(), ServiceHubInternal, ServicesForResolution by servicesForResolution {
+ inner class ServiceHubInternalImpl : SingletonSerializeAsToken(), ServiceHubInternal, ServicesForResolution by servicesForResolution, NetworkParameterUpdateListener {
override val rpcFlows = ArrayList(val configuration: NodeConfiguration,
override val attachmentsClassLoaderCache: AttachmentsClassLoaderCache get() = this@AbstractNode.attachmentsClassLoaderCache
+ @Volatile
private lateinit var _networkParameters: NetworkParameters
override val networkParameters: NetworkParameters get() = _networkParameters
@@ -1277,6 +1296,10 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
val ledgerTransaction = servicesForResolution.specialise(ltx)
return verifierFactoryService.apply(ledgerTransaction)
}
+
+ override fun onNewNetworkParameters(networkParameters: NetworkParameters) {
+ this._networkParameters = networkParameters
+ }
}
}
diff --git a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt
index ac91bdec68..d9d8906861 100644
--- a/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt
+++ b/node/src/main/kotlin/net/corda/node/services/identity/PersistentIdentityService.kt
@@ -12,6 +12,7 @@ import net.corda.core.internal.CertRole
import net.corda.core.internal.NamedCacheFactory
import net.corda.core.internal.hash
import net.corda.core.internal.toSet
+import net.corda.core.node.NotaryInfo
import net.corda.core.node.services.UnknownAnonymousPartyException
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.utilities.MAX_HASH_HEX_SIZE
@@ -19,6 +20,7 @@ import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import net.corda.node.services.api.IdentityServiceInternal
import net.corda.node.services.keys.BasicHSMKeyManagementService
+import net.corda.node.services.network.NotaryUpdateListener
import net.corda.node.services.persistence.PublicKeyHashToExternalId
import net.corda.node.services.persistence.WritablePublicKeyToOwningIdentityCache
import net.corda.node.utilities.AppendOnlyPersistentMap
@@ -53,7 +55,8 @@ import kotlin.streams.toList
* cached for efficient lookup.
*/
@ThreadSafe
-class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSerializeAsToken(), IdentityServiceInternal {
+@Suppress("TooManyFunctions")
+class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSerializeAsToken(), IdentityServiceInternal, NotaryUpdateListener {
companion object {
private val log = contextLogger()
@@ -197,7 +200,8 @@ class PersistentIdentityService(cacheFactory: NamedCacheFactory) : SingletonSeri
override val trustAnchor: TrustAnchor get() = _trustAnchor
/** Stores notary identities obtained from the network parameters, for which we don't need to perform a database lookup. */
- private val notaryIdentityCache = HashSet
+ *
+ *
+ * Handshake is also used during the end of the session, in order to properly close the connection between the two peers.
+ * A proper connection close will typically include the one peer sending a CLOSE message to another, and then wait for
+ * the other's CLOSE message to close the transport link. The other peer from his perspective would read a CLOSE message
+ * from his peer and then enter the handshake procedure to send his own CLOSE message as well.
+ *
+ * @param socketChannel - the socket channel that connects the two peers.
+ * @param engine - the engine that will be used for encryption/decryption of the data exchanged with the other peer.
+ * @return True if the connection handshake was successful or false if an error occurred.
+ * @throws IOException - if an error occurs during read/write to the socket channel.
+ */
+ protected boolean doHandshake(SocketChannel socketChannel, SSLEngine engine) throws IOException {
+
+ log.debug("About to do handshake...");
+
+ SSLEngineResult result;
+ HandshakeStatus handshakeStatus;
+
+ // NioSslPeer's fields myAppData and peerAppData are supposed to be large enough to hold all message data the peer
+ // will send and expects to receive from the other peer respectively. Since the messages to be exchanged will usually be less
+ // than 16KB long the capacity of these fields should also be smaller. Here we initialize these two local buffers
+ // to be used for the handshake, while keeping client's buffers at the same size.
+ int appBufferSize = engine.getSession().getApplicationBufferSize();
+ ByteBuffer myAppData = ByteBuffer.allocate(appBufferSize);
+ ByteBuffer peerAppData = ByteBuffer.allocate(appBufferSize);
+ myNetData.clear();
+ peerNetData.clear();
+
+ handshakeStatus = engine.getHandshakeStatus();
+ while (handshakeStatus != SSLEngineResult.HandshakeStatus.FINISHED && handshakeStatus != SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) {
+ switch (handshakeStatus) {
+ case NEED_UNWRAP:
+ if (socketChannel.read(peerNetData) < 0) {
+ if (engine.isInboundDone() && engine.isOutboundDone()) {
+ return false;
+ }
+ try {
+ engine.closeInbound();
+ } catch (SSLException e) {
+ log.error("This engine was forced to close inbound, without having received the proper SSL/TLS close " +
+ "notification message from the peer, due to end of stream.", e);
+ }
+ engine.closeOutbound();
+ // After closeOutbound the engine will be set to WRAP state, in order to try to send a close message to the client.
+ handshakeStatus = engine.getHandshakeStatus();
+ break;
+ }
+ peerNetData.flip();
+ try {
+ result = engine.unwrap(peerNetData, peerAppData);
+ peerNetData.compact();
+ handshakeStatus = result.getHandshakeStatus();
+ } catch (SSLException sslException) {
+ log.error("A problem was encountered while processing the data that caused the SSLEngine to abort." +
+ " Will try to properly close connection...", sslException);
+ engine.closeOutbound();
+ handshakeStatus = engine.getHandshakeStatus();
+ break;
+ }
+ switch (result.getStatus()) {
+ case OK:
+ break;
+ case BUFFER_OVERFLOW:
+ // Will occur when peerAppData's capacity is smaller than the data derived from peerNetData's unwrap.
+ peerAppData = enlargeApplicationBuffer(engine, peerAppData);
+ break;
+ case BUFFER_UNDERFLOW:
+ // Will occur either when no data was read from the peer or when the peerNetData buffer was too small to hold all peer's data.
+ peerNetData = handleBufferUnderflow(engine, peerNetData);
+ break;
+ case CLOSED:
+ if (engine.isOutboundDone()) {
+ return false;
+ } else {
+ engine.closeOutbound();
+ handshakeStatus = engine.getHandshakeStatus();
+ break;
+ }
+ default:
+ throw new IllegalStateException("Invalid SSL status: " + result.getStatus());
+ }
+ break;
+ case NEED_WRAP:
+ myNetData.clear();
+ try {
+ result = engine.wrap(myAppData, myNetData);
+ handshakeStatus = result.getHandshakeStatus();
+ } catch (SSLException sslException) {
+ log.error("A problem was encountered while processing the data that caused the SSLEngine to abort." +
+ "Will try to properly close connection...", sslException);
+ engine.closeOutbound();
+ handshakeStatus = engine.getHandshakeStatus();
+ break;
+ }
+ switch (result.getStatus()) {
+ case OK :
+ myNetData.flip();
+ while (myNetData.hasRemaining()) {
+ socketChannel.write(myNetData);
+ }
+ break;
+ case BUFFER_OVERFLOW:
+ // Will occur if there is not enough space in myNetData buffer to write all the data that would be generated by the method wrap.
+ // Since myNetData is set to session's packet size we should not get to this point because SSLEngine is supposed
+ // to produce messages smaller or equal to that, but a general handling would be the following:
+ myNetData = enlargePacketBuffer(engine, myNetData);
+ break;
+ case BUFFER_UNDERFLOW:
+ throw new SSLException("Buffer underflow occurred after a wrap. I don't think we should ever get here.");
+ case CLOSED:
+ try {
+ myNetData.flip();
+ while (myNetData.hasRemaining()) {
+ socketChannel.write(myNetData);
+ }
+ // At this point the handshake status will probably be NEED_UNWRAP so we make sure that peerNetData is clear to read.
+ peerNetData.clear();
+ } catch (Exception e) {
+ log.error("Failed to send server's CLOSE message due to socket channel's failure.");
+ handshakeStatus = engine.getHandshakeStatus();
+ }
+ break;
+ default:
+ throw new IllegalStateException("Invalid SSL status: " + result.getStatus());
+ }
+ break;
+ case NEED_TASK:
+ Runnable task;
+ while ((task = engine.getDelegatedTask()) != null) {
+ executor.execute(task);
+ }
+ handshakeStatus = engine.getHandshakeStatus();
+ break;
+ default:
+ throw new IllegalStateException("Invalid SSL status: " + handshakeStatus);
+ }
+ }
+ log.debug("Handshake status: " + handshakeStatus);
+
+ return true;
+
+ }
+
+ protected ByteBuffer enlargePacketBuffer(SSLEngine engine, ByteBuffer buffer) {
+ return enlargeBuffer(buffer, engine.getSession().getPacketBufferSize());
+ }
+
+ protected ByteBuffer enlargeApplicationBuffer(SSLEngine engine, ByteBuffer buffer) {
+ return enlargeBuffer(buffer, engine.getSession().getApplicationBufferSize());
+ }
+
+ /**
+ * Compares sessionProposedCapacity
with buffer's capacity. If buffer's capacity is smaller,
+ * returns a buffer with the proposed capacity. If it's equal or larger, returns a buffer
+ * with capacity twice the size of the initial one.
+ *
+ * @param buffer - the buffer to be enlarged.
+ * @param sessionProposedCapacity - the minimum size of the new buffer, proposed by {@link SSLSession}.
+ * @return A new buffer with a larger capacity.
+ */
+ protected ByteBuffer enlargeBuffer(ByteBuffer buffer, int sessionProposedCapacity) {
+ if (sessionProposedCapacity > buffer.capacity()) {
+ buffer = ByteBuffer.allocate(sessionProposedCapacity);
+ } else {
+ buffer = ByteBuffer.allocate(buffer.capacity() * 2);
+ }
+ return buffer;
+ }
+
+ /**
+ * Handles {@link SSLEngineResult.Status#BUFFER_UNDERFLOW}. Will check if the buffer is already filled, and if there is no space problem
+ * will return the same buffer, so the client tries to read again. If the buffer is already filled will try to enlarge the buffer either to
+ * session's proposed size or to a larger capacity. A buffer underflow can happen only after an unwrap, so the buffer will always be a
+ * peerNetData buffer.
+ *
+ * @param buffer - will always be peerNetData buffer.
+ * @param engine - the engine used for encryption/decryption of the data exchanged between the two peers.
+ * @return The same buffer if there is no space problem or a new buffer with the same data but more space.
+ */
+ protected ByteBuffer handleBufferUnderflow(SSLEngine engine, ByteBuffer buffer) {
+ if (engine.getSession().getPacketBufferSize() < buffer.limit()) {
+ return buffer;
+ } else {
+ ByteBuffer replaceBuffer = enlargePacketBuffer(engine, buffer);
+ buffer.flip();
+ replaceBuffer.put(buffer);
+ return replaceBuffer;
+ }
+ }
+
+ /**
+ * This method should be called when this peer wants to explicitly close the connection
+ * or when a close message has arrived from the other peer, in order to provide an orderly shutdown.
+ *
+ * It first calls {@link SSLEngine#closeOutbound()} which prepares this peer to send its own close message and
+ * sets {@link SSLEngine} to the
NEED_WRAP
state. Then, it delegates the exchange of close messages
+ * to the handshake method and finally, it closes socket channel.
+ *
+ * @param socketChannel - the transport link used between the two peers.
+ * @param engine - the engine used for encryption/decryption of the data exchanged between the two peers.
+ * @throws IOException if an I/O error occurs to the socket channel.
+ */
+ protected void closeConnection(SocketChannel socketChannel, SSLEngine engine) throws IOException {
+ engine.closeOutbound();
+ doHandshake(socketChannel, engine);
+ socketChannel.close();
+ }
+
+ /**
+ * In addition to orderly shutdowns, an unorderly shutdown may occur, when the transport link (socket channel)
+ * is severed before close messages are exchanged. This may happen by getting an -1 or {@link IOException}
+ * when trying to read from the socket channel, or an {@link IOException} when trying to write to it.
+ * In both cases {@link SSLEngine#closeInbound()} should be called and then try to follow the standard procedure.
+ *
+ * @param socketChannel - the transport link used between the two peers.
+ * @param engine - the engine used for encryption/decryption of the data exchanged between the two peers.
+ * @throws IOException if an I/O error occurs to the socket channel.
+ */
+ protected void handleEndOfStream(SocketChannel socketChannel, SSLEngine engine) throws IOException {
+ try {
+ engine.closeInbound();
+ } catch (Exception e) {
+ log.error("This engine was forced to close inbound, without having received the proper SSL/TLS close notification message from the peer, due to end of stream.");
+ }
+ closeConnection(socketChannel, engine);
+ }
+
+ protected String peerAppDataAsString() {
+ return new String(Arrays.copyOf(peerAppData.array(), peerAppData.limit()));
+ }
+}
\ No newline at end of file
diff --git a/node/src/integration-test/java/net/corda/node/amqp/NioSslServer.java b/node/src/integration-test/java/net/corda/node/amqp/NioSslServer.java
new file mode 100644
index 0000000000..d316c62c0b
--- /dev/null
+++ b/node/src/integration-test/java/net/corda/node/amqp/NioSslServer.java
@@ -0,0 +1,263 @@
+package net.corda.node.amqp;
+
+import net.corda.nodeapi.internal.protonwrapper.netty.SSLHelperKt;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+import java.nio.channels.spi.SelectorProvider;
+import java.time.Duration;
+import java.util.Iterator;
+
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLEngineResult;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.TrustManagerFactory;
+
+/**
+ * An SSL/TLS server, that will listen to a specific address and port and serve SSL/TLS connections
+ * compatible with the protocol it applies.
+ *
+ * After initialization {@link NioSslServer#start()} should be called so the server starts to listen to
+ * new connection requests. At this point, start is blocking, so, in order to be able to gracefully stop
+ * the server, a {@link Runnable} containing a server object should be created. This runnable should
+ * start the server in its run method and also provide a stop method, which will call {@link NioSslServer#stop()}.
+ *
dyTF1iF#%OoDR{9Y1~>eC zN~i|NHGRuS`E^3H1p5dFcem=|3fC4=ZEHHeKLMhJ7WTidBh4SN!&@{XH@HTQ8rHc)db^zWEPLzRN+FJlHsDj~)fwdE4kKlVak`zy zw_imK3F(bEX9OnUK*Vsw`8lom;FyMF%j^bF%RXp _1E-hT-YsBHuPUeNs?vR)_w}cnrmcr*tN#WT7;Y!AWo{i) zyLX6qJMv}B>ETz 7m~O&}V3nbSh{We$;tz%0Jd>h;0Ly2TZfr7O zq~rlvqB71mMrwX+LidgHF=7B_dYwZC?Nnd7c0U!WGeA$FyHK0>BEoio(UYoBddcrF z-i?BT;+M-t($4pBMqB2|a0%3%uhZ&2Lfu=pc5%kSPALfpN<*|FZ`m)I5n`9rA&k 9vCokT3Vee4`rpKT%y^fgsKxG76bc-q z1OM){{p~X6>btUT+P7!? mP0EanJzxR Ned+)MD0gq*USAS-VC>MG`e7P# zFO7J)S7Z(9s2)gDc6#@ST*S!k;xp5suwUd+gA_~eW>fMP%d@zrPMC@S*e5NxNkjoE zu!?GoWC0b;uVZ=|Xc@huBIfZ@VL^H$V^rf*7K9P<_hW0Q5{PJWdnhc5LgrakStM3} zswYZr*NCd?wML5! u+5(9 og(SQn-Ydgr^4-m`iI=KA|OO9r^X&Bi~uECj0p0chkl~>ks9ylA!=5gFx3c-P3 zz&~X&L@njTGHg4Z$*D%LfqO;lo%b6B_ZiV(1L{mwHYt1bz9W5i8Znxqb8^STI)6rs zA>5`_qN*!+ zJ*%?iC3op5K?D?yjaFEaO06JP>Uag!pGVbQaH?wKq?mK8Ryr(P# zCjNwG$$l?yk9(|!Uy(IyWC!bZq79&Oq)7judy#A(ZI!}>S*gey!Qjmdk0pysOF4pn z&mPUBjgvLR@$^NFDqY4gOcYItWKAijk=8fPQj#>Br1%Ms_P iurz10JY^UaHlGT~o@ 4n`$$VG zZUx6~f>m+?PVMx`V}gNI+RG-_NDvsY;o`ldMx(u}ghyQmEu +8!}2z9#5{;^7$?I#kB=^f1m9Juhd3a@aXR8q7Y{#Ie& zHONApQBb!TfejjM*`F=pcVqdf)k@Sn#yXk_$#ZI>YTLVqtX2-%2W)1*k5DTb*LGt0 ze!%!i#Fzks3=@^%t9QuKxQJOj;nSgUsly$^Qyf|(OGONy=y9hquKQKjuz0g50aI@x z;b F6F6c*8(8uVP6R}a3u(whU)d8@=-*wF4l?1*F z`|LO>i!qpdyO9T`tNG|R{P2w0YOq%)e{#XJR3>qPe3X4b|2JUAgMwH9|AZ+bcq&83 zza||v)gB+|cdFC}Ff`yoc99?Ddsv5jQ3au`yv#35$5scSRs=<1k~A+IfpAzj^m_F7 zne15O(5vdZ8AHM=5O30*bjRMYXye$U^vn_0PFoQlz~={8{|^i-37@lD>#+cSfbf`3 zKGB|UQYP^h3DL5#jI!!iMH#XGED_TmbfFX0og>@crIZg#CoRBheK7WbeT9r6Myqex zr?6GNp<8tYcrCR9-|r_g7#i3w8NX6#>Vd_dx%%1RSx&O#s1uBB_KDJ9JYmn$c2O#V zzL0gQ8Z`X_YN6?|niK0oha3VtHIqSlC(6Y!Tu<05N$+vafnD~A&@b<7@*y_RJ=9oo zCb=8+1E%a?+Ymsv(3NFX@w#WmXrAHvf>q&+{FQCLQ3G6RnU}Yj7`Y*XN0JDHQ`7*1 zajhQ}I{mI1hV}IaVT5Rj6ba}8Bxl<%?%!7PZfJ@Ms`aaq1~o>rqFf>95kJ_bdk{jQ z%H4lh?lt6-(FkQvlxTNJMQx&X{|xvkeR~0mDD|QXX<-9QMJ 17ULgg>JqHOtIxvRa1SvHlLbhx%KLZ@?x?E32yg6&a-qK^2fuPm%1D zwlr-e9dr-ng Sdeg2w(qxBTGf-~S`P&Vii3<6R^IF!MqxYs*e3ys7)C>}j9y;e3Ymy%8 zZSo5DT4|U--Q6!VC~m-86HF?SVaABlW4#R|r-%cgYUh8$wPn>B81!GfbXh9*8wnH$ zC=VhPKIR`!D6jBO0ZUfaQd;;YVpCM-Lj>!!yZjKf&cMm>3$%q11?HL=g??eXLmLG1 zOVdj@-1c4M@eP@oqF~@XZs8rdv(li%r7wr_WIDONY}|OCSo{9G-68iP;4(Q|sd`E^ zsSNCj_AR!wUEMx9`2-(7Mx~Kv<0wV`7T1T3#yW4#U{!Xa0em@|1X|UA;R#hB2|LR+ zv5~Q;wMavqp!b{X^7@Y=R7L~`0R_&`rom3#2_G}3*CQA&GLR?t5-&21nkd3HUAJEg ze@Mhgt4I%@j<(vKUxNuT>K>&J3PMJ+;|arMP{g+05}7COiD3om$?J z`9g~O_#OzNyI(7A4eH_t;ZNjV`G@<(+W+bf1np7 z^uy{TXGXTWj%yx07BIDsIR&GkOv2!Yh!4*z8woVI?4<@&lLc~Vg~B{N)Ku7*E>$Ps zWw6ND0_4I|4j#$-7fMXke?Q5jbX@#7qL|SF*E(QIVnOkg_mx))U}23w6a8t%#6)CP zc;xk&il-xVWPF078M`sxA({!LXmF0WwBp9!(A*4$X@zvx1Bo`s=oZ@U9$+!_lM-?M zLjub;l%n)KnjD67CV73}c~7PJd)CSCTjZ`S7JyZ}lqs^ZvCG#xx{oIOxBqNyB-^OZ z{(AK6t4YjQ;EOfjx7~C+PX5Hu V3T7L~H06as*Dz z8!4p9Vv#zO`#2lEHdZeYT{!+#?h_^17x@3)qEt!oe^Bl$6%u@E#~KLie`)OkK*-D! z3J9nXGZFCu^FNcs{|8sP)IQvlmr%cDO-b3(qx|oGR|g;w#}XjNK}ZXNfl?sf10g92 zSaD5m5(g%7Ib7p_NiFe~*)%QBlSxWxnTuQmhC(1vnzLM4uAE!`Bz}X>+U0tg-Rna- z=s$%z%~
{Zry_6?wpO8Q^JcC0j88zB=Bh%8&R;NFZHporrFlR zjYOGOLaLoq7F;E$8>x(3j}5@Zx5^j%PAlK5{_4c2e?thnKAtljN{g$lAo6kMEyAn6 zKxt^5mL*CE?;25OOc_g#gqv}h545GUwBU!Q6fhbao5-uOlGQ >rU9Ipkxz0@nxH0b{XN6{@_k~2!7h#N#EeO@@Ws-cmd3<3TZfHjAm*O~ zB6SkVgKH0$t6^(=Sqw%{Y}jW}{SE2+$ TzRvau%%Ybbsv_N zzXV@F{(vF&_9F cQC9QQ7onZPASPlNB zeUQT|$BE`wvE1lSws26W;a=Y{KG#y4#AK-A#d`e9`m+!h zFl5O*q3+@j|C;uXTwpsFbZnjsGR9CZQ6_arY0=0Gcx&i)fD9$LT3$+Vme6>)ID7eb z;p&1?XK+Qpc?gkOp~}8Y&)hUZkUAO|P{H6-y4laN;&?saUj%fo9A&*bX|4=w0dm4b zVscpQp1*K|L!hoOoajZK; P-`z}ls=C8#~t;sAcuD2&+s(7f! zMEko4U=7qE6Pd6ob2T8$4NT^30d9pwD}v~Vz!d78dkN1ejV2-V5eDV0auY|)at$f` z&@qbDR%Obc=E@12k)Q>i7~69VQ)U+ahAv*A<{F{5Rip2u^>TprPQ|YRaC_wbmfI|O zJ~=joG6HWb7sFGxQJxIkskO7(xq_ZhBEe6e;I;)3j935uPZ032-q_7iK=Vig>p-15 zv@hAYDkR;(h(KqM8cIlHg?e_d-Jvgt2it_fUc~X{RfMY+mGh96hg5;{a(WV%7>S0~ z;R0!{s_u%La#+Ou+&3YN3gY--oZ73x1?qKn{6_DEH-*B8_1}FqkgXj`bmnR*8@RTz zYuG)v*DmWX{~nB?G-?hffTZJb?EN4o3Dgfn6T2-nsxleN9OFq!54^`2G?55FPaeGb zwUKK7w10)YIsBH*>&!Gtf@Dz#iYzLenZW8#xpY{y7>erTzBR;jy|X3*m5p0^U`)48 zSpCbL0Ky5wFq`Dean5#k^;H*-*1KS9Sj&V1b}ATWEp!;|U)ITSK f_!%@-_C)StB%*Szaq3O6}KWD z;Es PIZPGdKawSz^ihPy|V!8@*131O8qNu5Ls(X`-g+7v=fEJn2-`m7oZh1Wo$@4i& zVeVoTxmqEib< 7F_ z9a|pDAyM*iivgnG!^Roi=|9)023EYhRLpSl4j4yXw0Q^Ixa^gBUH#P!Xcu!QVuI zoPFCfOFv~ffXLOBQY&nir|)PZbZ@DNKBOr)8dtK7$VRE66h1o#9AvJfX&Gq}Puh zOjaGy@ZP?1ypak|fIjT63m-IxwENd5o2r-Cy G)FP9oBJVrt>|^N7 zluAt^!vMP(bl-ah&e;5{KsJQ&8||a| WHeZ%+oblnXY=n(bDI`naq;vk;4rSgsb9d(LXN*v5K&@1+la*7A `L&I-ybxjRW{01*cYwU&;EhES3TeaR*IG$o&qmmmOsMdfYG(>;Bg zLttMq+)XJ@GOmzlI-4PDa}cM?zB4q6*70!{P_&&I&G0GVoU9g3DM(SvX}_YPCa2^B zRr-}y<&B9;MRo;6xKe3DInCbwrMqQEL|6= *q~vlKx5 ?GsqHK#7r0(J7_8Xs@WQ4V_-^KIJ@HyiS;l#m)M}}f8NUs7dZp){Hi_P?%Oj`8 zhqi(Z1{ZuXfXi;S|IKcIMt{=LkE>e*;KO*HbwpGnucNMtPr%bzTJu4vT>!Q1QF{@4 za2M}wy;dP8mj~8ym_FH?Ww#g-83 %oRKvHJi2i?yhqfSdsT_x*;<8z>O{&-={+Bb6W#gdi1<28l2=?hFJL z;JOwk4kV)mXX)qD2W^-{T*QyxP$$tFq+VB@ZB07XYihcceq^9^71 _jb9N?lr@6vib0p<79e&{VTN=WYrG< zvzg0^tie)h%BV?dcN*H5RZKGch=_>{F!JEaU8oxTb5Rz5%|P}SV`^E(XjXNE1zd1f zf5l$x+|N^7##q^in}ObuUuq+JtDKV4%OeiDBP4_?d&)y=hIqate)_~Q!+@+AZ*JbB zx$#R}bK+jc%&oL{JY$ja7OGL%(SrsHIZWA!&O+UlmQO<0ywKY5*uLFil)H=s@XqYj zkG++TS%@)ZDV2WQ=dCKx)r+$vf(i!y8Pn;PDo9mVaesHxDq}lniFD}anDwdCp1-l> zJa2CUK{x D_d>qT9w(VT?bYK1FF;o@ivP$sEbWMHw$Y%7GeMN? K!mB-Ny`)qR)c*#Z?`!%uYdQ)5XD{#(>>;?*dn13vuO~1BJ+- zN9C#rBq{@(+PqUL?9;^nTHqNr3vlRKr!I-ym^HYY7#h$wtkx&D&x&8wHn9OX%Gbjz zTtV5Z*uqF1VEZchX-rsxT-RoTFEGxFnn_f_6)lbfU-C^_!!lkq{56s!XC%S@O4W z{0Bu gavg(|^6wIbmLsLSt5 z64CD?y&%XwHneuir0e67F|bk58W}b=7(~Nk*E2bYrORpzKl0mo^yUwsPaOWJON3UH zXMxb7VCUho_u-Z@t>;+bNrVv5f-r>93<3SKF{?hdaW>a-i;;;0()?!Opdb@2m1;8@ zuJdsnzZ^RrmV;OUl;AI+aec%vm`mBowaU<^HYWA9PwYtvP6UVQeMWVyu$%pz(=cFv zjR)=`lsMB5DIL(oR*~9H500>yGc$8^&)>Pbd)vFkVGDOTyvpx`5>dHE6JL>}0O`}x z-jI8hS#7x9(XM7w)^l=t>J{Id-F^K#DCQZJl6@`Z=gQ&$OYoiKtFjW~Z4C|UF!9FJ z?IlXGk5w_b=gjrwA^jt`;K@!MRY1>7`P^8lZMQR28Xk!l8UhNdm+J0w1s3XO&^vZ{ zZL-=7{p$=g`Cw&|MHE-(Dn`H4D+S$`wElo#r^zzUOw|X6Tg*wD(`Q1}s;qvw(>1Zw zA;wnowr~faw~$4#dcVB9YR)P`Ww4QP$M%-x#o$#n(*el@UkOBxE{QK_@rUkdwao~` zZN(lPyr($Auq?!)xRF6iRh@8}BMW|?npQsWa%Y~8zO~CN53r#RS$j|nB)d=eu*c4H zfMlX4BVd#Zrbl~Q%pai=BUM{r);NdYJ5p =)7pIItkU9?v1Wi>P}NzJpQmrsmJ@BFe&%2|KX$UHRy*;L(UMs> zZfye`ZS`k6cXPhwGz9g?S$s0_c+5h6D;IBztn|!ILPq0a2F^*T|B2QatUfs#i|m *bIrivqISN(Y6HGEo(|yRM#7OK%KpTWt^p7UFTy69MH_I zI9NS0BK2am&IE>l8MV#aY~Z7l)pWU>rE_ 1VhDRg>lJ6S#NKZqTMMo+UUY?=%K _uDHsx;l1{$fE- 5F_;Ygdv?Om_JU;x!E%mK*35`&Im82TR1CJlBKdC*#^?udfmEVuEb>t#^bHW zcA?Ae8+cQO`>g&z3M0ktE--7p!tzq(;w8^wyXCiuVxcPD>CNq#S#x?Xgf# N#gfuC3A5Asmk;y(R3Ii)TIcQcq^8NJcs`+6uDf>6uOZ@bb fQ4j68WILR9JJ4>e`u%Qn2c;S{i^7Ohip1k$4&)Di(Msf0h$kRR zC*ZXczr{->l (Mm8j#!tLEfpI1l* fAu*lumD(~9&|6K%mHaRuFR1%?Tf zs9#{=oc8xyFpFNo8aaT{%79l#5ri4~Ff(n0nO6Mn{eV}?#L%DmljqGsW=Audk~o?O zBuTrjFi+7__ Ey)_M8UY z*~@2;=T4j9hl$HK)aOr`XY5PXRkH|+Zvr_vIstWIP0h^(uRCfvj`w2Po<;D_i6&>Z z>e?bn%n!W(<#F2rmNUlxpzMTSsSNrM|AlYVfNGV0QFe26Nu$EJh%iE}N?uufg}V?+ zzItO>A#6-{urtkEU3uo m_REUPsTE19wM}>d+kG)uaTgS&!4lbJkTUot+vm z_6Y0%#z@O~b0bfzSG+x*BZmD&4VRqVg)NF`!mnlbTVZxoHpsTR6CU2;r8fi6C;)-m zjIX&?4vpn<)0@{O6dW 4Im-r=n>Ypr-I z0T3M0^|&egU>jJ9WWs^d5T27z3cyfN6xcw`ak+G4oD{~AkS-06tGiCTQC?`io>9T< z6ZHt1EHMdMzKZ8$wzxqz=nZo{xRy$P2RdW(!L45_x1{y*Pw+2Bokq)Uy$5!ay4KT3 zm$fP;#`<4gOy+&OA^STUSfdLR?R`abt=Cyxq`wRJB0 Je?a7LLWw>r5FQ;bbCAX@} zcZIj2?bTJmoa_%KSDtUy(%@Z1vjTGq8mN&6P_;!<_txEoro^^0vH3lvnvrq~Wah&~ zPw~U{C3@httak8IN7;nN{A^dL#&+3Jur8+He6>?*>cWfkVd*(1CV+w8NvrTo4GT(h zA=WI_QFOkvmC^yG7c5tt&cn2)(aq7xYFE%dy`0W9`af8(jMM}lzlB;-9ZnC}Se}$c zd|<10UzMyYnStPr%FWib;xmfWdPZi&Tx2gI6n$D?A5-8SeMHysn<)1vIZxj*60{O| zsg3NY-_)Vi{ZXY5@c^T5SW&w)9DvXV_t-IYS_2g&1^gZGA_q*f@<~L^=zZ+!>5HW_ z(n9P+g_v@!f!sF(I+;HV3Xid4%+)qmG_?$EU_?(S<%ckrs-^L>sJ4RrMZZ74^%GjF z!p$=NJaq^r6x=Z=%!LfA{qJQ|EwU-`0taeI%K;t)|F@?Z;R*Qh|Js``%YZNcm8sIe zpo0I-pD}}3!Tu}Jl0eZR{*^b v4Qp!Ea(rAmt%J~!N-JAc7b5q)kK9I*`Ld);b+%f!ag^@ z3U~R{kJAm8&tJ840}~3WkexP$6K-2|@AC1P38Hc*PK;7{Z_r1ncj=f-EscCnSKT@3 zRvc^Lu5~g0M3qWwrQYtY;^TyjT-)TwVu;z+nqSl8qTw?z|5R-=k+P&Ws=NrmkE{^v zz8$Vuc4tQ4ZuJ|}J%fKUWcY>3Xz`8k%3vA{dRNh$3>SH1A~d*y7-rZ|$OQQZ %yN;kX?yj-}w7$Y5Zw)hyu58GX2Q%y`DCj#3mh(R4 zEb1~o7_=A>dRkqMEs0K;AgdOjg)(9sKJ+Oai4|g6y{xas<}PWaV?nTrp)84xF+m^N zHq)=qj$S*&Yr?dl!<<9PJ| -dh6@vY*MO{pa@J zN)t^bPHnIv2+Jj4tXwF6oEdg|aTEFGIvguwvcNV&MOjl&R-s>;9&ap7*YC)sB)Zzf z%H$Y*-sE^qx|zx+A6J@kxM&$Wvu)Hyp8J=hh*!U~(fIPw2B(D(OS|)5zF8c2(SFXQ zJ&_!6PMQ$34 7baZe0ydsZ} zFl4o#n5!6QcHb1F#sMji;Xr@9y$*Y~vs3R5a;UDTC@Aj{0k0ARttH~j(kX~>$|GsW zLNjCAqB?!SWvNo8`#UyS(}E>A7p{et27G}D6BDvH&!Co&mB~Dyof!BbS{5yH0uzGT zs<+|Exjd1UK#!mrsTMP{GE9W0Pd=1*?l&1Y@#>{IS|x?ctSITtZUmmVp})I~e3^F2 zU&gbYQf-WcWF2`IZo^JvOdVExsAJJGtz%u_To@77h#9yn6}pfp1?|h(L=-Q5*5$Jb zYaiBAXBKTbT!nBzNICUHZeqY8hSOY=b2j-~;b{9sLv`N5h<@fK38xSCye;e?Dj#GJ zwZ#&C*|xA&mNStLh_aLJ`6C6px?>jsJoF4drRko-9L=Kz*_Lz*;}rFTfkPaNC~o(Z zMCRFFT1?Ral2yJ~{ILFyLI&cgrS93IQ6)U2{EDi!)8Fra0Q5uIQJkn>1>Nq%l*bcU z8g?;8LRB&|OqgZV(RHu6aoC+Qkd0`du>J>Pq@f+Nel}i*yTW&}X*qlnGkuaAqGW z<9;2dxalu0@_wqB-a4apJ1BZqrMmV}gJ@bfQFfTbU515r3vOABiQSR-&>3%cyzdcJ zJ2Vo1k_VUpPvnVy>0n72FY~-}Q(yXWroW||;4w8!_y$j(p|F-^le U0fGmJum*HTg)`50G} B}0`3Q#^ZHNGcOY}T)!z|*aBeg|d-L#2ZfWpY$i z1Va0Y^F1sQJ1aAQ91|;ONWXA&+K4@KxSJ$P_?;jMfP{=GEh0aG+5V(BDwTbQ;)(7L z!_g{;pHSYT0JrlvVldM`VtTAnKROB16C#8aD^Sa%_qTY%@5Preh=X&4GAKPc^_Ux{ zOD}=52$$^%G3-_LS;ad)gCwsbO#5qXnv-x*fObYg*2Ui=v9eJlavvCe_o(G?rB)X{ zNeL$g*e}kG@0hiQSQU&L+z6Semsk!vFJN6NJ@6i}*y})pt0CEQ&!?$tpsm7XCw) z_?8>AnHe^C+^j-lQ)!&ZC)Lljl+iPu(Zdiu!8Z|iU}JEKM@kP*bnV~GnOBY>)ChNP zK(95T36y7``F*q&A2@_9&H}Rb3Mq;qbwGRsz%{G*;48GvmNhJl)}WW|f HPS z>QA!wSD!JgQ{9on`HvOsAe)HaB23by%(#vVU_2&$9T7n5a*GnA&&iF$jXI3jsFAc0 zNKoVsvCs)WvJv L2+{=*H@ >5m1xS z+UAGV22R~XI#5G|j{2i+0l5hhwYRSVjq9M*&@(|=#FQm{lj1MGI%Fk}@G7VavjPWf zoL0`Lv#JE-N4Fmtd`V(Kp6y) Z)gRm@whPW!Nso-@GVaqvfGOL zih3+Y#XPy Klb$lQC#4aP24Ao}E^Yi-K53x)Nc(nm-91}D0o zyIbv`6$3LcuViS1%OhR@STI#rKe!fkVxu%K)33>r-R#m8!$q#7OUVrQYAAo90+ZH9 z%~wfoBJg}IK12~3CsBeuJ}UVoMt!+-|Dq-sS`Jo-Q4TQVETwK4{wDM4c*XjZfg0Tw zXhkQ_nBGZIm4D~@rsaTn^Oeyh3 hysV3?FS6B`vtYWp~~-yM}kUB(HU;E`HEOc5CM5hyv|$-P-Xh2%;= zw-=0% m$X!(LmQG5eT*qOtyEkm_vpV|OtHvL!kh^0|jDnq;;ML7Ue+*96JO)qWpvPP@ zovi(d=HfdD7}`D+VjIVZEtbwG>3P_=2cbECWzJ9L*>kU>irb6$alY?x461xr4@{f` zO|0Dg?Y|%jh=P#r!@nK83 {+igx$q|3&~W5Y?ZgK{k){Bo z=?9X8`%vG$TwGtm!7@)Ro$LWUlxGde=^_1>;Fg7Jk@Zy=e-_wS0d7%VT9za5t`b~F z2P)eZ)_IX4El#!l7|cJ3HF5i3Ub|6b6*ojYKG)e6uqc{Wi>Vpf&b`9g&I;Xlh~Svy ztd2+)K%!tW=tQ0U^FSFKR{7--}-IR=SvZ77Hptj-MaxWGRBFxgxO!UH(AyZWu|l z=dN6*q`X3!mwg>pgqloy>Cu=rua_Mz`j9nZiQFV1d&O+ikfrgGA$HWAgWl 9R zZL)mBXelhDMd1{Y@-xq@&DIgGWOnXQJ)5fojK9)&%o-g|ZiV*Y#m)5JxgL-W1S}Iv z0<{gDhnWZq(PNvRpMfxHo8ZqEgoN#&S&Chb{^ek=V!3|M+djQu&CRCR8U$-ItL8@` zHfyR#1l|jkC5x^SR`$D|k>#v+rOjb>+->vADmO#p9WZCVjyy~?j>;_^7TaqJi%^;Y z6y8zvakk-sdOG4r9V{+Y504;B*e$CogqDgn?53Lwdf>@W^9d)eJSv>|W&~`Mw`Moz znqDRjtrL2~lf8Kba_GC`rlr-=3@1*g%bGANeNhN wF{N<0fV0~r+oGxWNULNIqIjhoE4;dGdU^HG*Xu4MX5!v7I zU VBpgp0 zgWh(SWgQqHxDi+eI5hRkP1(1=XKy5jR481r;nFetA}2GF_wD|$2vk >A#4B|g;>sR7&S$QB(N$hlIM zzjl3`1MT{fqsn>4M?vyfv{E_Gmht2xoUBQNz`1CYw8%K!QFcw3R)Hr2j8-qmW?%C@ z_{@LHu$lylKx!7Sn%@g1=#7=mYPug_y3cT0mPVF`(&lSf@LZKhohjVU9W?sq^soM1 z6qr%+(=C)WsnvWPK~Zmne{L{(2IJ)a6;Eqh%;^NnDzSF!@}9Gbz}3xvTA<%yVmgUQ z1aCmR&>boux2EO={!9}D$X+p9Z4n^k5Qt?~O{?JP>R-Gpl)<$Nz@2Ecv-^=656cmV zPBZa}>*f)V!_CXx1D4fCCL3M5o_=)Im46or@KrH#(L#Q7jGnTotnauAt#pxUDjZZO zxG6}s^$Is~mGSTBZF~W_6EaxPnHB4)bZE~W|EzIrQJRd!PBT6Q!0-0y=+h_Utu(Xm z{&{(tFT=EIvwjh4LEW)?dj6GMNv!rYgm6$r4b0e8mKuRI&XWT6^@YIGIlLftgS{47 zRF@313SM5-8IY+DF)$Z!$SF{m9Ju8ZRf1P~)OuD*Nhx1LHo(bKu>dm;mkghi=P`b_ zzEfKNRhP@+M5-+hC=HFBt1~KkBKpLjJF=RCJjzwzgxg8&L+&T$W1h~lJ#44iEmmm% zv)z_lQ&2l^dR}aGiWtQ$vC f98x&l>S*i+Uyk8%f9SD@@Iq^G% zv%~e9lEp6GGQ~kz;fvRw5P*=D<2Y ~Ts>9#Dg2`Q80jRT!B*%OMWEa*-yv_C<9LqB? zMo8?ozIbWmNo$jUO6l<5cA$cN5Nn!}EzPl{ebKP>xwZtk%+aojcpgdGbGxf1S Ih@PM%r=Vc2RS)l?AXGh*zY*&n zsV2wpaG7grqM4!hsYXgM$W1`D2`$=H?VWv`3401?c=~g&=rC0mEhSN9N2x(QTFs8p z=9PHm#nvkE_<%ySvVmSZXF#bY_8f!2?R~Yx=|G!zk@ca%<-{iwRU&zt2A%+^4ODdU zSDp6(WUAGlGDwNg9B_n{IHFR^1dXo_j=H{PB>Pu08A~InN6OVt-nNZMoBtnI{}`-E zxGW9BGt0JZ+qP}nwsp_4ZQHi3S+;H4e&?LMBfjrBe> fkqRIAigjIq|CF0DmAIsA8C-;R AA86u+yT;tXDy2_3n5XyigVNX`EW zO{MmEP^0G@k0->Jz_tX@lvhfg=KIZN%vz~5L=x G9f{nM{9GZSNFm~c0yPzXU+&OpJ4m1l$3G2V zhVgg3GL1$F^9bU0dypmGPeP9?!ykfL`BDYWV3ZPYWX^$c11|Xc);&p{qi>F^rd~?3 z=JWus+z STCyBclR{5NNVBY)ow|cjD`LHdol<_Wr{c#?NrnSQsvZp-t z7+bheg?D&29t+$|emYGzD!@wAHfase0n?64VZ?UCPK$#kBJcNrr6kprQZw3!?@F3< zf697z2@bQQSfg (Ce0IPl1I38p^;L76&|kv^und;_MRM|AYp^i zt?Fm2VmpId5giA(-jy#W6BvnjMMG8zXIzr{F1ly(ba&3FgN#@9dtvguM7$(i#GZ(q zFLN@9L4AFnbW3VO#VA*UM4&9FC+g4Tj^L2I#fALdhofS-zSi;$qt|f`bFV5-n;mt3 zxl?~3x_vIuK}at=<&Xs?;Sd85Gpjt{Xn5hBzmY1O)$Zof|M^0 kWlliZQd%Xle~zN2W!io2(-`TDJ!CbKKH?S-t<4XXJK1Nsw8SI}E9{h7@+w;?s+ z UF=`0(~G`M)U4A>%te#=C!kwr+urqzvPtod|I=NjFjG}t zk6WBswWJ1b^Rx-K4kJoHjI$=V*F@w;Nvl;t)D?o)6@nG KedhR+EZOTnX}$ z75?JnJEjxc4hGYpvLQVy%Kx`l7AO;n9>q(z8P~XX=IX+%=)D7w%?elL% 4xHiHQ6L)Gh1|Lc7Q&ld`^>Us4%?=*Rj?)j*9Q@u%3L!X@79uPZv`P6j?rOW z(?ghv#o>kPn#C3H0+ux{1lkqy1EJnDPtha5nF0IjR`MHp -|Yui0BK zE{Ly9T*9`+v#qS2AY*8y?RW@ji>}H=2%6c-DXNm&ch+vNZK>thhkIQ2wxIgRjmt0K z%=M&WpBpsph#%t6*DY4x&st(iR5_xeH%BG<+1aK_vJ)`CC%NoQfJtceE={b+IgaRH z8^oWcG_wZ_t-quY1#^pkE Od~2&57|vcC=%`Uhus0)M$J9jZ3bVNn{pJg|uYsg^o6HR-DYev0A7! zb )>1>nGM1X4|#QYro-SnrTz&AfllpVLqcm!tOOqXk7O_Q1r*%`^w%$I=v2df=>N8bW@>?< zQm1*55CPe08}=xw7(dzb*G&^m!DwaB5vq!<>@5@!Eq~^Lv~-}FlA%ydxp+IXnYbIL zC()pwwr{?FzuUa)AnR6;@EPrQzV99hB1ey;VMM16mGv@rUOt1VHLRuoGdF$sopk2D zz23m{fz+YeL%3wx5m|eL*h+JsXe7=*m=c*r-vhGiaQJVXFwa23fk%q!$z`n;;> wvs8r;i$_+j;&Ha}Kh1^>B7<0mWyh7=w0#x((OjmmHP#MR-go;x?DX79dr+ zQ~`qY0-;zSRKN~WW`I+|drgEaEM*-`DIJ({Xi5pAeCJt5DA+=IF+5Tduk|Q2OkU?F zJ=GYJIWF6OJ6pF;>#-Ya$h}C&Pe{c6@%$3MwAt$3ROU*pD&Y&Euw4x Rv0yNRM~ zl+qQJ$*ZqoWw~xIjaJmQT4ks;X(x `A<%+&CM0K99W>PI(D;` z(fTlj=I{25k 6t0Hg0?C$`XJwz$;yC?fJkpy`*IbwjE}^iRt*% =h0$I_pb>Su#(NZT EKOy9#te!5|>grNXqn5yOR%u!9pnKD1gvIcArff3N4c@}r7 zQSle8KB8|@3Cc2f>b~lfXb}W?;+p&KB$j2n0|d&Q%j#bdM`9fR^FCS!R})lc6nO|0 z$r)vLxr=7#N|xjSlMe`$DCe$Gsc%UylGR-6)Ww1gH^*ZZUDCoX1exYbcBaP?!+Opr zONv?IcC0vKCR5JyMONu(Yc#-nL65 ?p%UehuxTx~81Y47 zQyzb+AwE1Md-7V1<*GRBjM4>BqZ fGYCgFD_!Pk2YmbBu;*l@%Jdd6I%RUMI8=ZbZ8~ z8ob_szp*X4 goa7F9<^vP?6_n0z zn8fd3t@i-uyKm!_IqLq`>VODWG~)3!&brVC5mia?U-<(XcM%ER@X~=UnFF32xJI^U z^IIBQrA23v@_~t>r-DyUoxDZrB30L_(MrQ<``w>rhSDseNspx7l|kGq{r^`4z?d5Y z{%h!;Bqs>s|L<&$s|NlzfQT6hymiP2cpCUWXFfqQaPEJ_0oCUq`9S}<%GLmU{2!lc z(kv><%fBaGL8ac=f(W(}*aM@1{BMrfNFeC#_~*Tj1^fTW0$Rs6fp`CNBRS2UsNKI5 zk_MD;{@>*AKQGX_`5QR$H^l!;Si3BOs7TGO1(pZsP^xQHf-0gc5*0V&fcg%kVH8Np z_Jc#fXQbKGQq$nMdwAPEmfk}9JPS(tFx)NSn`UhhDGDlg9lDvjAAcNYreC(+ 7>!1n|vLK7qi)GK>uTAueV=IZjMT9n+}`1d7vQGM?$LaWD}iqHN)IGg_t-hNp& zZNLV&wkE4dqHs$fkI2tPdfOmooXDLU1TieL7XkYP)U>RJOhb#b9oUl9L%FMkwtG6h zTjleruvXGFV30 0Jbc)a#vp@nKgrx}D+!n8DcA#7(DAWPzYmL)6kI2iC z6yKbZ;0slpP`>HI(L0~ONr9Tzy*>-b{@>77&giWU+@YCe2xPFT#g$E`APpO*UDh~{ zK(zZb*KKVP)+N%aLhCD#`7RmJ!&RcXk`zs<6zpCfn{~8 Xfcyup zP`#rIbpLZ0s9~itnt}c|A_-Rh{StITz+kDp5=dZxZVgWl6m!fUS)??R210}wCo~YP z6p}iSS*M~^5(Et_AXy~`WNGd-TsG*;H4biUIO_}l7tLWUtL|piN;VxxB<=Z%=GOIP zt;&|Cf@ZColRJF;rx#ylZxi?&lmZ_32j7pWkM5ff@0&5=A3kUNKTEHn0hIxa`nh&z zb7hDC*|d1S!3gXg-gvv3o-oYTYLQyQ^&V>^FP+vJTWiaR^E(e|8F }YTr96ROHdX87kD`gMRa3C|jP_#qwJU z3MetuNvrr}R@e>p43};o`3&tzrMjA -pOM%lEYz_VbbwZevS7XYnH)S_KMgEt z+EqV^N=iP2Gpb6)h~E#z 0+j;S7$M0vHHEQ2 z2#E=3swcBi{Z #=W=IBAol=9S zd|!;gQ1mje2Uo3#ABlB9tc;Mtf+-9e^%4s__IOVJ-J>wX$;OC}(HvFh+EuZb#To)E z+M|ooY+a3p3hm&J?2pX|T0F8JXlhO )q^>`NH0#Fn~Q~IPfM#z-kijAqcraX1B$d{ zdxBcEy+xgY`KpDD7*AS5Fz^PZTfIMor~f!h#E7qVEwv7JoaDAj8B|$$bb}e_jEn64 zKCe!IvIc)C4i}SZ)lM3g8D$56TJMb_GlTw+jR%4_KMM<_jS%(pK2Wd0VfET}6`d|* zaUi%6ETqi5Gk%rEVO2--Meq^A&Yx5x22ZqFnGgO^X0gaHC bgWKNPTgMqv9yiMftSi%+oV&(J8Q2 zHN-03!fF9JC^^M!f|E!;ZUnicmK_QHJ%MlvVTxC|&YdSJVWLeT`Xdhv72oLg@2Ptn zn)ZN>@ &oEjm|yg{U5g&*CpoT)9<=@fg9wmGxgh;`j} zq9F`XDJdV;!`)Hi_NY<+<>kIe@P>XX3M+m~8WU(4&=O=R?iCGyLDA2@-tMBoS=lb$ zlk-V-Hj5Ef@!{2k6i0VO-N7)83-Q>jGdXLaVN~{|u6!gFl=cM1y6$HLow%Q@1J&*1 zOIFsr3O4*3u7xuEugop>0k?IXpI^bC%HbF`Lc`!#`O1c=n0LBk{J6}}Q@96nU2NtF zTFG(I&m#W#HP!~;vBy@s(@lZU?a}Nq0wr&)xx)~FUOz$s8wR-tMym# <4I>i1V z2VTIK$OOL`1rM@{g >h)e))-8~TMBwygFM&i (iqJ89e@jL6_R mTGDNR%u(@lO&JK@qs(P)=*R(F%F zNzT(}Oq}7rSI53V57%B2PE*!(hI*)I_uitU;Tl;W>Z0O=f^_w%Ih1LM*i=7|=Skn& z95T`Y`@2Ol)-CaA*nhOsuN`1@%#Z>S5vs7I#2f{P_+?6iEwWXU;h&TwNpbuDC8Z%e zjm=Cl=|Y+m{~;xHvO79+s#+7ITZG77s**cYz!~TfsBr>kbX8BYtgQgWAKj?hptq|d zMad6(QI|tAT$9sDbX_|HrUXgIjvSJg%N}(v;5vnn5pHl{d5Iyo(buvrc!?u8sOjlT zb+rW$+2?oWN0bU3*RPeLL4MtYgIy^MxM0~7h&`n=Em7{)^+*)qp)X$M0hyKT0qpez zSp$Dl8Ft5i&~>df_rp_4I@s6vBFM6;vcnk7C{|VSMq5 Qr2LK9t}p0Q~ItqTNKyxQKubtTgHfPB$iOGy;&F>KS?u4MtOMZHMG8NKLr$%l|= zM?+7`TLkp@U|8jKq>TJHa9NR!TZ1#{Xyq7b$HcbTOx!8@$TXP7?kd;$2D^*uu;T(o zuLcB=uSF&E+Ddp8ls5RnW}rt~=B4y|@H=A*bS}l}J}w!qVZy!;dfZajI>`kRarvo1 zXN)3n<1e%}&ud!S!Lnla XP#sk4(32SyI%syh|) z$0dAsLnr#hTu3Lq-oF*e(+$7e #-=u2ej$&_(*|EeMea?Y2OK@e6<(YjN`sb&Ofj?l3 z>0a&&7 f_jOkoa-dbd3eHo7J?>cim;;!}9oBh2Y)96B2RX>6^^DkB zKqD$U(qmNJTjBRmB^^g0H~sX`$@FO(>bwvD2$~);P^;tp1e&g#>p#dF8t%N9mQM)K z*Mqn7k=l10;rOD|c!f_n5`-2<4-C+C0 vwla-o4)cmld^^ zS%Jv?m+M3qfYby3FO2~oBWTS0H=NZMF_q97{=c};4*&w*8hZs&3Gp93q*V>f{a +b7`5%A^4Kcv2EVQ zlN|4x^fAAWw=?QLo;+q6)yq@F*G{8|as*e#dtL*ZjnpWz+Fm`_i$RZ5Cj TTp?Rejt@PO0iR^-tFl76vf%k)fYynQ*BeW`bbN!$#yq57M3?Ic}z~j3NHdU6`VV zX+OCcSS%rcZGNZ)Da*pg<@FXe=;Ebh@?oGsEpsuWQ~UU0q|9J+7+u@qrto_y$4&!A z9`CL4ig4p=kO5 -N|Ctb*$ISNE?3EAShEJ&Qo}+giV8l*S_6quf|_{` zLuzbqm|7-UnF)8>_S_jatNmcLyHdE9u20jY^;mQOD_7J7*FJ`6n515z|2&{HJ~#58 z_mwmxU)oG#CIzz~-Da)Nc2m*npy(KX^$_4xe;!mWdCHZ}ZYLm~+Q66@WY}jB@FD4f zeQF1k*pR#IzSK^$u9Osj)m~q0rn 4~0%vYdS3={h@5JwLwxyA()RzC->c_Pb zuT2$DZ^mup8Pjy|$R0)uUrF{WVnp;(t=?Ybs@{konC~&rFlH7*)b$J32@8K#GP z==M(z l-2@Gq7*(f?DnMHu>5jb*R_K}@aV`VYNvQBlYI z*|K5A)FUYR14ayrMdJCJEZ>l_e}F}rn*q~`EC^2HU}BP(C(RT2xLfjfvumyEV0VdH zwRX9*tst2sc4xbSG`nrJZL`(Z^jzV0;mITX#ocS?jVu!(2#y%bNmu*MR?p1~?Mvs* z)EQvwhv1Ler=Ec@%@O`QU|x$TpPZ?Lv8t4It9J#%;DTLdLe4y)!}23g*~D9@vFaTS zKBzBZjm=tZey4ilL+Orhkm*uoJy6LXl$oPytrpfA=pq8?MliT9%W!NvnZ$q ^X~f5rayimJL)-1xiP;2(lRQ1i}TSWear+kiWM6$L#DWK8m#{ z)6te7{{;_kp0^Cif;J=(`~l>6%7DDCm_jASEW0F|?@}=O5B&~0MI(i9p0(P6URjSV z(6$CuI*U#AG wP7KZj42Jnw_Q&(jgqDftBR+Qr6YerjJgmQI>n#nVSjM zAQWdB3+Oa1ft%I}zBGl+T&bCN~sM90`qKL*j*Kmpb z;SZWcE#}_8@+F(hV2=_+b-n~%KM?%Du%o3R_-jEJ)hZzPD3TA9`Zh!=DNj+57EQ83 zvfrpnPN@=gaqlVNo?2sHi57~^bw#(vfHFnWDm&ol`VtHjkeaWjqz=KtigFGXZcMuF zLosLu3a$ICPUQisM_pdc6UbDNRv!@=`i>^1I+kztwm?+ZAfRz?BgJHpL8+anNMF~y zXTyBW_fkJ@KgF!8n+6Bxl&ubiEZ}Nlez-pEAii6nU=9#BFVgLIv%Ir~aC9OgfFIxy zdGC)($(uq0$cc9u8}#BeZeY1nx1rhJ8-jaz5Xv6Z =-lqlFIEw+ui!(7yLq`g zxg|Ri+}jgs3q1$82foa0wy*5Ai51vsi=Rq}(g}HSH?cADytF3KIJ{zZ+jfx|JFn=| zq604O(C~Rq)g4%C;`9G%_vkn`R)~-ICrVLghgqBe5RoZtEiMc5xBi)a5WIb(mVKdI zB$Ew$CnudeOUHD2Ic|b;qAal`yPcWRz2Z!k-I@J-otY0@%Cc<8_5X(qUY$6VB=o)QokDJo0t*BHav6 z0STgC&7&@xQdgla&s&l^AcLo}ku*R4=EX~A^f7BD2*v7dJBsSsFM);UOKSbmvi!M1 zCzf&6ROuJLRkYkFY_cE{?bK7Jc%ih;`dQ8d$oO^H^2NwYnENu-&KxP_694RYWT?3* zeaRm%D}8B~^XU@BWgS(#=`W$qlM%N~g-YMHcgboM%Z_6OFm>_A(o4ALPtMd>+o|1> zy1!0z!N5?*Nfg iBE9&h^x&$8*nQieclO`5QBNVk2i2)xTf7IZ5jB_&f-re_mE#`U&_^GRHR+?# zzv 2ZtWd@%2 zJ1^(1mgxq!*$bJ{nttnk5#S{x_YZmkblD`-0W+vyU!S*P<@>avI~c*s4Z-}@fG}mG z{X!txlXW6{Yd-zY4I*AaX2UcuE`*g3o9x=JQIN~EWKQj6QJ*Gm#g{ht(mN5#@!$R) z@noji-iD+v3w#u>jMV!;x_kZ1G#*7Sp=o)GLeZqbUnnzgw={kuSUj9nyPz(BvH5S& z5Nm)_TC-VRcQ^k$_9<*bc!+oH8OiYBftuEXkc$dOV$pZ^bz%!yQiNqVAFVA1 {}Y8Auq_J;RHEEInvv& z-xZZd3x6%&7@V~DBD35iYr0tg>@(o;3v@Hl4~&ORJ|^+_zS5cOv;Dm!tN}_`_z)Gc znq^kDnrM}a!AF`^iq_N(7HZbKnpLXS-I`4m&B>Zo>ejxRHY(O9CiRll+Pop`73Y#w z<3^EM8KS#wz;EiG+0x5>TNcLWR-?SHtlkxcItc2HAaJCDkfG;!TBLA*rEt1y)CQ30 z{;p)-vFhqGkPdM~Ou=L?AYT+O7-$YO*AX8a??w d_@|Y1vL;RM}|Tvtx~4#6_-w3a{i>j$0m$Q$ZC{ zsN>M@W*YDc5k7Jq1=6DgcE=G4UIrpWMl~S10c`QRYebhrF7jJ|e+ImoI?o??ap|r6 zSkVI0q733WCpK&1IjFO2iNympJ+|t2MW1oqFU@w9n#9J!HeEKC=WGt5FRmDFF@{4W zeN4d{lmKpf;VqEsh-DvQa+icDjB7s&sP%4E^65|;s3D5Xr^2*VSD58bs~a=QxU1;T zYX_VKorgXOJj{N8koy<>{l&&n*k#Ot_!cq;u5NPVsAMtQ{RT@2dFN39B!y!0ak;AT z>lj6jx@Vl9pAVIa_3m9yM2 d6nX-yOV-BhJR418B}3?E}r$Jx;|fPv9HqWq%|` zn0_4&08;#{Wuu)-T8{pp^%eX3kNT~+Vl;pF(QBhdU~icA)oW!N9snIUYYdcURC_Ex zavBiK_=Y539A`HZEJEpb0HT(TobRx|`jt6BW&ORZ1*X9|x1?0SV}%E{bCab&^~3)E zBKptM#4XT&J6h(T5(UNo3*CM1fw5bc-ht8o1JY$mpG)`QfBh07`Cmws!i|KQimeX} z3()d3Kw0tq-p0$=TATA93Q+qK7|k#*CGn^JkEnrw&VbIH5rjdfo}0MEt(E#VFo@;+ z5MgxSLWkr6@MZ~}tJM6TKuKy52S_(-3mx)mZF0-=V=LTN*^H*>%tg2Gv6t;AHR>xV zgHLf==8xkk@0}Zuotw1&nck;i=wE0NumGUQ{j=w94YCxOj4_|_b$hc0Gw;~u(B3OG zs?3SQk4cpp-i(>&%yJPM`FS0CvX6hOAqW*3>09`hbE1l$TkO{wXQED6+CWQ^XzUU% zKw38Qsz#Q~crYi@w#sW<^!2%#nNZmq+gV)85Yo7&h5eV 2%R8Jz>L9k3f+pHB)N?VSBlK7D!bcr%-Przl&I<%8<8y|RWc_d zh@)c8nAFYgmS>Yr-_#6a<8~O!W~Myi0>9mrALxL=sJ !Z0aPk6h}yBM&2@;$tNt@?3nyh@j+=h z4Ypy~ 0!cTL<&KyvjhG9L?+B32sxhs5lUZJ* z8faYpDy_2ID vL#I1d*`T|NRcCQ# zoZ{PH?JR9=b?*4jULRjXm;k6=iht~JBO1plT(~Spl#aG^$re>((m6xcGZ)&M&wQxr z%?RwCrNJyT$YwowJ1p7)5mgFeLy~0*Ka9t^+KWR^)|x8YqvpXn>a<3wstjwas7Q6s zqGZ2axzr4JrGt~B@nnv?rm|4=)1_vkqU(EjA=OGSk+b3_i?JO_rvW-0@%{Z0E!hYc z%%)_vbW@XHY{ro=`_+$IATfH@ YBPeR_iqkALta$JmaBt2%eihyP4JM~0}?&x zt33YL($sQuGF89k`2Ydp{LcopkKXi&5^hB@@S`0p(w+sL?(M>>q2NGe;tAmV&u0}~ z$ubf h;PFNetMu`32<2m;fj~Vv>w3n! z-S-glucrRf?ntj#+cO!;BNnjud0iY2o>90leNBqNJ?tsr2>?DaZW7Vt&|5;u fco-me>?=lDP|G_rqdB?;x5>s@HJUYTRL( zLwV(V0kFMvPrga@goSYtW@7=g;7VkoJ6^h@BJud6 lOoG&tB;#{A3r#tpahPnb_WE7On<6j!Rj*UOZgoT~JR zn#-|t%;Zu~o+3c2|L9$4!qq6KPG=`8PyeP0Twfpl2DH3H0S6sQ87-@bzzwwte2bms z#!TYq`T}MOYXKAAI|o^DTco5~198U9Sv^=9Z^+8uTR?(wVjC=Svy~D4kIMuZ;WY06 z5{3T#xGG)EO(E=>3m-B&yB6gd#!Wc#K%y#^N<;RTgJqJeu#pD$LX?!djBM14l4GFw z^tIQT>t#|84LkYEN?q%;S#>w1XO1=V86WV>YV=d1N*jAhGjbzL{(c+{I=bm2^KGh! z+{iE~H=v@>vj=kcNEjGE`mqY;v(^XdBU$ri?Jdjxm33yQH(nvs)+y8H1FD~_{Uc`J zi1}~tP14dOkY19~Pt0P>Olm6VVZW=FvR6&ZY<(u=hNPzEy*MS05bs-71_1bnfc1wg z@f75OrHe-{(7Kn$PkOY~)UKMARdvu^a&FNB5s _N026QE33sZ z*1GvOrtvS8daxD0juFzra9J%uMj0_BB)~F^zzuZBgi-W(C%$Ff+;pd;Nq5>>G&9TA z`STcg!J}%;$I8SgMCS9XhmCVBR}voQG2Goz_Mjq=(^-`-o@4bCnZ43p+}5~K20NBu zgsiywG&24&s3Ce|L5+y3V|83VH%*k8`7KF)63rq5f(dbA*el@-*mlDhGdGQx8jvGt zn`xR@g0omKNZhF`U$e3=NSj^;BlqYio${3Cbzz^y9!vjG^V>C@pNV%T$1LW_btG|` zJS^qD+i<)*|9Q+V;;C|4;kMb&X`E+P)uV?m2XTGdd%ZFAVi}RMk;Oafk*@%BfL& z7{jUfvin#pN(J coNy#ll3yY4f1Bc z7N6O%F>oEa)oU38ZzyZxHevy%Nhd)5JBttA*Zh@cJ=Ebtl!i+e;lO_aUqPuvfut^u z_vUIySL6dt`9vaqQEZw<5uik@F shfviSDKhjGLcOOX4Ij&oa5h-*lsrW$AuR}Q0!)Nui+i1Dnpimp zVkq2xR`U)`5NxC0ae5Pd$NdO}USC6i01JL`WC(p57zhqQU=r>S@cAsJoKXJd#z7#j z`3Dy9@Pliz2Wz3q<`DSQ#6(2k&(OJmuHlF+K!8UPq0a=31&f$eD2G2s6m1aVD-@0p z_&9@hqE)j0uq)wK06Ky;NFEi9RB}UGm=B>HpK1K$abGJ0KLyMEz@9!9A4g|Ni+>Q^ z=P^}=-SY=;u4e 0wQ6Y<00P6Q%qt~{TM{Ec@l#<)C z>imLwM?g^bxb=~TclMP!Blr?>jp88Kt$Vu%Fmy48?Iakaa;yO4Wa(Qjij(u3Yz_8$ zG0~gm gHy)h==XngFevqQpLZN|L%fn-btplqgdsYA-j10q-bVtG$6YXm&4 zHsrLv)Bz03H7$yMR~|7>4EoO80|?8M+j(&)y%0>ESz?Cig{gIp+T6tI KGlJ%WP zoQ{<%_J#Rnd~PA@!Jxh}hEHbzrAzKeHMQ`rJYb}>tX@}?D}TbBjXQJyV#sj
B>Gu`W)%1f>mNAN4_Of#J2r2F*z(WlAQf7KM_GkZp@g`hR#dy)0Y;dxWq6 zd{VOaMR}r5l7!Kj{L~5m07`_Z+d7j!euACBIoKsa$Blf7cCU#&L_0cnO}v*V&6lX> zw_*}|YapHqgv(MrZ05%zM-i+ZHlcCx+(X$F(rL9gu zI)+NS6! ulN!HOKdlXyAhLUA`o1~%_L}%7D2DASHuo}G1V0@Rel!>8#B0awMpYLj zeQj09pZKLP`TexO#>MV8@oOv_5J!c48)+72pcGoUWi<(SVD3gV!t7B6L7)%-f=FW_ ztLH9}usj3h4dq;5gLW=-$t=;R4)SiX=UBb!LH`z^F!p|KAc23;f%}TR;?zLkh%!`R zC9SuD1#_s?P-nA2{VWlEVh+9zsP@eu>K_Mb8j5xag|@_-R9SM8cPSlfQ2uVCuF3O9 zor+hj Jd}h&kR7#lOLO0+7gk38r?oB0w zL;`_76M(zub;wI9ox4&CGZ!hw=VVyVk5AFqYDOKv#iOYL)mj$AwN*E^2HHyobTp=9 zajPvVD<{|9cCV@`vB?avXOzAH6nIJnQf)`0n43gh7RA(DtIbsie9w)3o5U_`*ad$F zd_kdKIMF%|^?!4sZG+_lHZNdvQJqIQBF%RZRYkiVBiQETyk3WR8V10^II7CxSeLM{ z6I; AuSdMLVpV8D+H{qbvraSth77Vy0tnrl76^fy1Tl9ek(WiW_Z4AyQezO zd`-Jgx6hnx|Ew#ExWN4+o@GfCbjiw}*QX&`$BMIKjwLAqwo!CL%{^Iiq{uh4qdhvh zwk29guue`beY10zSd2>gFP>z*JY{0pQ|WbDx5J89ofuIcuQ?UD7^irXRY#U> DgmjOEQF63e=)w9Wi>+cs5jGomZkQF*#O8@`!jKhto1< zq=_v*;fTyzQ0W_ Cgq}Jl(#15twJ}9Rl<#n!Ra~l{* z5>Po(a!X6KWb#XJcQPb%4zw15cne7KhQ|G*y4!T+&&ISyHRg63ZUb%yPz}*#&_{1F z_|5%M48ws3p$B0nu+Wx*PE@99D&v=FN(f_0npGRIQ|QVo^r&tOiralVI)5*mcT|Lg zH17oiq*tbO+h}+~%g==`FzT`qqs14P)9om^p1N~?72-q14^QL|6zm`QWr!sD9+otI zv1E)KDxhUgxQNX}>^*DPC=y}GV~=G_Ar{i&$_}w+0O3-Jj3&|OWsIn3xbQ$sf|h(( zP)!+RcvVo%LQv66mVrf2n`e9zs^rhvlq|>t1`x5P&KfxrZ>p!Jo)lqqcS{#6(kyD1 z ^QEWcWy>8>sqyU`%7{c(O;5Iyc}Cb%*K z5bItUjBAPx<0nw-){8JNjDKs%=oYk3ubK=oIm~8EhN9(*A>!5iMNW*DN&MbuCNQ;z zQaZ-(rHGM|XM@f))4-F)8Y_x3qzG*1&`N0`K~4>`N*bz0K@Ufk(Ht?$V>A|bFkxXG z)mVvnkW4>GG#d{uNx>d-4NuxDl{LEnkP?t(X=RtSSM7`jUoU(u3Z{;uq=o}*7>Tf} zAWWK?Gc5{y>{NoLi&87TX`5(jjDkhrfjQDnBCcNrWv@IOQ3
l%8Hs4lvf#kDQ5d6FNp#d3q_^7BQ#IzV%ZC JwPJRCA^)6NQxZ8W7IJTZh2%o16Bexh7oEe8zVCRjoiQxyinE zr8Ar|TTtT_AJN&!9}@h&{yh{AHv$$(EJWcZg@Ndkj(sclGRyadD`*g#Qmx_pcB71= zu!W3E;-r2Z$~I=)TcDN5XhW0$Hn&F?Caf=)*0lq?lJFngNDCMIzXfbO?)kZHiqddf z^6(7NX_*$W>|lj4tO4UIYG9+qU5}=S=)b6McC;Syn=8zcSiHi mHFX#5-OO=}#{Vpqk6$>_g^m+bW#A?#{y@z$FdAW`P*{(afTl z*Ow^x>@G(7F^11;A<%Ygh^y FZLm3MM{;ub~o 8F_T;=h30A}XheTvfl~CJ0T?K8;^FrpMcmF; (8;9Capus|=1xDygKhXOY!e-q|uGRJ>-E+|nkItu9P!bjcz5&e0N zkKGn+3UF3_;V=28EyY_Gze&gIP;+V~<0S&u(NO6kQ@(V9X(PaQ&Lvp#s%3OCksDh! z#p3}THYYrh=Hzs+INpGYcK$;TF~Gv7yzJtWnCrZjJHdws_!W*yQn|-HAA+1T^&_pz zZ9o+vQgI#tE Mf8)sNe6#h*KYy33R^pPk1M zW9sJ3LY9ibGc}rwWMO(?Yh>VyviXq|iw}=ze(RYA2rwt|qp9HY9DOJ@Mr}%$OQZkN zfdvyhz+o{8oOsgc+&gJMMx(z~4BTz26VG0P?4ux3RF*n;b9lJATNWX{cfaUM4-oZr z*gzlkt2yw(r{85*c;CL6#NPWs4S{B+4S62QzQ5;dg#>NNYJ5OFx1c|P(V znHVMn{3=|Js36^2zXkQvD^^0%n_{t*^VX)p`c0ZiDKN`GN$mj%G)@4gr<+_H7_Gs$ znk2!61log;CN+dbi lhE!Ae(CLqU*9@Iozl7bkk z#(8m!LKcE)CcpK nJ6Fx5&5vKQK{K1EMhy}c9?>-wkkwy(vFOWycJ3&M z&b-66w;z6=OUg#`^lVr6T@A=N5sA-xdB0`p(WeQn)*DujEU5iWEv_7ia61yEcIbu( zV2#bKRjo(0H`Y?99wq-J35J1v&JrMnj&_NFO?*w4kf{Kfy`fB?H#8+lTq2#4B?odP zZAqhjc_c4&eh}B%*OREZ>g+nSGNG9CQ5M1vE1C{ehA AEw&19rfJc;vK6nE*&e|RzjxI6Jx8P@O9k+MiOW08EfrE42(haSa6->K*c zALGT}kxTUT!QSzwn68WMd8sLtW F6WE*nd8fBq*uubhS zKG5as$;JN_`S>E%I z@bZN=u7rk(nIH1Fbk*Yn;#^T3+5k;5IUC~FIu(-)X4JXs_ zGA;~~YR0_je}nW>dOe{mQN*`!)%)ghiLI*VO?h4rp_=jU<|EGU|HaR5r}bh;vc;P- zz!@Cz_U;uai)02-7MQ7I6>LugI1Q|HK#~zeDV-zf7qj(ICMu{2^xD$+|JZuRAj!gR zTer)$ZChQoZQHh)W!tuG+qP|VRhP}vXMcOgjeX+&&5Xzuk+If0=A3JeXOPXbrAqaD z;z?kMuEB+-^+gp$m11)pN|X< {w1eO5>{S zOY1|(=(H!A7CXZw@jhh~rE!0LypJPna@o+iJ|T!2vy3YWF5bnFY);K;$pZ|WxU8+h zRC9HIa+Pb@1!N|1oeGxe`(IB-thSXZ06@N(7L|IL5~$ +mu=X3SbZSO*(G-%yl=C`jq@m3T{RykVSh> zJ}H*Rl`>Hxlr_Y@jTZ3jD(3lnB^2pJdyOkH2=**1G6>^=humijvjQS!2ebMLpIM7B z-poUu7dZ g28d~E2{2kUKTwegdxh!R4?>0+rm!6CZ84Xum`K27YBKpF-O2r z)2<6n;>zuJ0YeO_JfS&VY?F%?sbZ?wswj8b9&EWzw>YC-Fp63tAQntszYW@?8`4f( ze%It@G$-zf)x}exX#vzG+dPs*XsV@_EYRFY$rj3ewDo1JtG{H#2rf?Gc;(P^9~*Z5 z2=`Ry)@-hoZ+w{%ct1zUv3?)+9<7;S&J{0lS^IRb?FQYs=J U!(%kTL(UTZ$O9zvkvkarA-E z&Gh^n`>6a$j6Q0cKRNZ#g9)SI+Hvx4t)-Kn$yX0(Z z>U-7mk=k-Z^=wy&tot8zB8#t8<|^$;s8@CJ$V$T`e(+k&TU3c{h2ag!H73vKidQeB zG_iGR+4|C-9K0NHEYhh6sQ2&J1{3MRz3PA DH&;jW3Z5fB;fd$qKW8{rzaHs^? z4QsL-$J^#jAF)b0GZakVv6|zakIrEn9Q2Do{3U=}Vje;LlKi*Dyp#qkqTsUV9#QdB z!X8}TLrbn$@~>>Vx3m&}WH5ZKAz~D$-&@C26o;tYE6&dqy$V=Bl__0rN^Vp=pn3I2 z)4iB~{uz>D2J_}k(Y~#2)u(dE-uU~Yxh>N(@5Hgpe3JD>_T;&{ B069K#R{&YPh>b?EPYflE}xKv-Qz0FIh z+tNPC;MU`HqJMl!vgpU_F-kk*z?b~O1kbjVPPCOwjnn!-JWUV&A@9hXcdCiUnSS-` zQn|V=5J1eeNAcwDkd=I#*Wev5xJAJN>K;LP#4QXa_}(e7ce-@DK=2J~e^ku6sG9|T zn*c_fF+zYh=3l>(izID>m-R&Hw5>o&o(I2@|5nUHx8%6k7I4RPUKay etEPR9&K&YOJg^<_1Xzz4h7j}3|X{PM@zf?feUei3Hoo>q}0q+X!9Zzm65Vm>e z_s#2?`JE=&!o&NpEyfnpefS<9Qb-YyoVmTBE_=oMRUe6|cQnRf>Rk4E*hf=LK(; z1Y&lgM;oy54gpCo`p1gzeb|}g>j4PkFCb6C{vQR8u(&@>0`o-emqQK k^+8tk%5HwK*8zSp z)zLI}3)(N%oyV2;h}u^^*ad>t%_BblQoiS;8Q ?1sr+e!;U zrzv(vh;K9Fj&h91it&=kpyL)CiU%Mh2t=8MsZuxn=$IF!nS93_3AQW3r2bK+_=1b- z2HWq8SwFZ5FZw>XEgfkk9BEaG^ek7J6yVB{4+7Rfw8Mm`$0}%mULKimqk`K0c!R}Q z(RNg9cj1pS)6he5LU;egFqQ5$Pfe9Wro*8TZO)%Zaw1Ho%i~HBb7RTTp8}|QzISP! z!{%v?GIw{3A)YKSRU`16HvV(CiPjanqud&YF*& 7ADTFb94 pk_<09ayrML@&L6`yU@4dt4u2h>Q#8>MH@@c4=YG wCocf=@o{?!)xtD>F~wBJX9et-D`u!v1dy*}`O3!ry=_X*QmDwKc@a z( sq+d4)m$m(grpD(-xnG6O;l>G~%{Usb?o+4p0krwax~0`+i?gYxhoS z9#h?JzWvcTKD(-M`bSi=A%YaNz&oXWnG$q|EmL~|7!R~c)H=_8YX%Ij>wLlUYdcnM zqVIYpkb{ujcBr3#C2#;s7*Ej3v-u)ncv{%ZNzf;>1tmg(VLePkxDmcsk^#b!V +c-k>`-b}1LI$9MaL Z-jGdQH zInP-x0Hd%#F-K)VY6{l;_qf#=lh&5i|VV0z H;ngIyzYqNukIl4?a5E`t^dv zDo7uK>O9+|%034eOxNb~(e~(^VlMZsFL~4TIa;fba&yZZ76tQZGG^JfZ1o*oQ2Rre z5ByE*7ZUIqHQ2q1%Iz&rv=D)~gH#EbLge2o`udj<4#~c!X%UYa$l)SZ*Qmx<{H73? z@MBi@xb88Z0Q?HBox36L<6LmsHwf>+O575*ak7Y2`)CWWTV!NfwzXH%UEP7bx38+D z3rC44U!apK736^p*_AkY>*+#2SIjsE>@!;?R$EA33qF79pKK+d=zIa)>)(3E)0uI$ zwA1bXFKE^a5w$gI=#i13n0Gs0&Jh36i%Ll?!jJcn(b5M4AgOrF_rde5d6poJaJNHk z2)E?b8+HR2fodf_M8|h1dJ=nOe`pTfkBN6W0qP~VfZ)gE6WCG@dA_%E5a@@KzJ_h5 z;pK?s2FabBv{4=B!=&eiETv>|4zn3iJ-X$FueHriG+{;lp;62&&sE}wxr =`Hq1}eC;zqlIw_CC19^C?H4Rgrvxv0+|b4FrVGvG2OXIev@ zEI|1O Q<1Ym_X*RWF!A?r1w+n^>EHdqNnEdhTG2 zc$m}>mS`8uu^u%q_yn-M{vt6T-@>kn6y=V2+MBv%ESj2^ltOO~*wtsgt&3bs~oK+Zs4Sx5cRt)_K4)+j=C>3@Ux5cyMx?kYZ8nfAJXmo70>)1`2J1 zo0jJHvqtykoCZN*JeX;Po=NdA04H4-^g-%_W9r(`cv1^-DK_Fj&0BWIy?}%wHQR#{ zk`+d5RK{%hFVhKxTC{?V)4oZVin3;=fXxB;`(+hY1KyQLt9|OjNTj*Z#G&C#4#({u z4}+N byFz48Emi%z$r8m`^Q}36 Ik`P^?3Dw)Zw_(22z&8j- D6G~$bWi{s4|ZaQ zP208NRW8_CT;TgG7+8k1Mzxjt8Ax1iobyI Go-wSX0u=*M8II93QMV{jF-#)^cy*LMo!m!cx%7o=T}Ah4%{ToX|yUs}r7N zzuL|B$#~h?CY@A7>W(7{L^S{(6tNF9-w&bcElMm+A62RX=RMgv@`;sna)riGS}Sc| zqv=}=-PDbW k^stKLxOr5+QKC5&X?%ts!CLwh zo+C8*0Rm|=?S6oe-3KN=4Mww^DnA5fPZ3WC&T|6T_;GpFmJW2kISGj>FaNKPsj|Q; z-0v{|Se_q<|Goct6Ez!=_H7O74XV%c3eo0k18Vjkvwv{*V~Y;>uU~WsX^!Fl!zTJ4 zvp*u>Lm5RK_1jJWeUKWm(4s{QhK!I%t)a!LLQHYt7j-_dRu?UX25i^GHTXy?pS7<_ zjju+P?;n>xE>|vBASPC}0BiGijHj^P*&SI4$SI4cj0wJL&NJ@0kDaft8v+2hLYy`% z(hB4lnsUU52W^41@OHCDyZU9Rx5g6PmJBSwX$SsBhBDR2?JCkPnkp5IEg9d=?xcZ( zo#qc2W9WvO5CVc#D)#OO9f@J3ju8CNVFlbJcrsCl;g<_YeEitHd=+ywlo*&BN9nom z!SAi*$KI9&$-A1GXN{zUIACy2xG`@`A6+DHONj{8^lc-`Cq|QWs^iM=T2ZWHSh6HQ zkimG<2V0d9=L#>^PB+*lm+aaVcxb@>;{i`%X13sy9afW~(l3HZlj&jFJ63ryq{xva zpU~|5d8!c hiMahmUqVtX5GBr=XQWx)QnQOe8xiTb{!E_8P3t z;1a3;6ra$1Au%Eoq|2q{Mp7-(g{>)oY^dje6}xH9?ILhyBw?i}9~;$r1T?Z07PJgs zxdQ(qUm2t<^B=@#63{ &Aay(O0?WFxVPHbHoQm5Z1|GaFhr+=4HugwjFj4#Tt_tVS15r$e1~%EqXh zIhx}3OZ#1o32x>w1<}6n{lSe(n=qE)@j)S=K1Vk$LLM{85<%9g7I%OIycDT3nrVjB zDrA7PCMfO-@V!Uerg&sux)6tSmzpjrA!jP#^%Di)8>gdW7ovA6Fk+fHEU|{eT6fDW zJW6@@S>mEQa=rBcD<4x>Y%Inq$!nqzxpv9?Jrbv^Du*CINqX(<8Vk{^9%qaqFOiI+ zu+WjIg*9VrCj4ynSD9J@+~UwiWdZjpnh+nP6O4%(k_n-hH$^8AZ|P(gMlNC_!Oy5V z5Q8bo?)GC9Jds05S{E%0ZDl(r6f@6u3_eg1`BI&`C13iw^+mUVKAasA3ll+Qq3K?p zpy&L8QIgAk!&8`ckP^G5{}*WB5)v*&7>Vk|rs^p5y8MdX(4NT%VDsjvP88x0jzZiW z@diHdZF7+8*UFO{nyR;87SNI_CD_ko{EEk>T%`&vFMUWICY vS@nEXHp?+)n;P5 z%hk!gxLP$M%k_DQdr8fnl;tYm2Bk=(v|3(CH(Vg>D3;i?6J)n$k~_ RdJlb0(Juz%N#m_)(e$8iMoM%xun5Zn6P&wzd*i&_lr<2+Z`>c%-ZOxJA-^ zkig*xjJx1UTa_S?H)7i7fzk;jJG>&pDrh8e*@s-_X5-KQ!@gap{YII}w)P#_xT(mq zRDWLfY4NvFv3k=PC#3eV!vc z^_6te5gNu3fGuswpmGSSnM($dHZj6!Gv?{C4{WJ7EUPn^)_$nc7GR|d&yS2*$cSF} zE36_b`mwA0d#D+`u&PTf*qS3!J;!fb^7%;F9NgmgLOxH|j$c9AzA%W-jlHT&9j~sD z=BeK!#>t6EZC=;nWN*xo`ZPknG2YIE@Wh)C$(qz0U?<9)>C<*s)@1xX4H5yxch}0F zW&0CS_SI2w#bB}pII)zkX4Q*;dQR(|ZTvYUN+GPg7xSXvPP`!q6iGhGSz56vW}3?m zZ|nYBXTHUMTR&GI7hsmci9+d@$K4>=?6%a9cgOI*wMv~SijU- x>o&-stC z*P?FefVzVIZ8x_#U5p|m1Z;RG8pAO`3QK)c0SX+@Z;U_TkA*%GGdnF?YPg6}=8d>p z%IEGW??W%|xaT-P-9j3vrun*-_w7=lRP|Y)`aMq#km$(q?qZRaKSL8eh%4^8;eE-z z$$rRjzV>;Y>be2Zh* E+2YyzFU2-j^e(PGhv;pHO<@x4JdSVQ+O%_acCY3OD3(F z%FgVmfoIRpA+idRrJ4stmim-&`l;MFT#bi#e=#S~_85iaT_x?ZY;<0ush}_dKy;j| zNKt|nC5ND1{QFWZmh=OQ{8C*VL47c_H7b;If#2tM;1a*UbD>&+%JgkApX6@KvUVXM z2ePnFHEROv7z`|2XbP*X9_|{8OZZ=My3y%2hbt?7Hl!-nk*HX_n!p5eEOgX2II^N~ z$;;_RI~=eiqV7#bIB)*~I<>kcfH#FO;cAca5a)R`akw)Zt(Z0_^sIThI)*qJhDd{q z#EfuMd?XX--)zBnh&kGPH0Jmqps1~6euDfy^>343&yGebre&rrU4D{7PNapXQE0_h zl_uUQoT{7peT%w%#Ro<@#K7H&gI1F~5KY^Yi2Bvg4xzH 0oz~(#J2;qn1xQ;H3*rJ3S4U9eF40uh zg08_I#t6962iS@A4dX#v`tWpg(roslu3~<=grgx$qzTCt7m7`#hL~CGV2w5y(a`72 zCQ0`k)M(-zf13InA;rErKpXwjtQ2k- A9xS^$j<)lLzWSL79lLGs{{)5eINLmIW_TI8=#mvn zdElusj6lgZADIzk?DSd`UmOjt*eWTsh>>$Sg~uV6Pa%&0#BJdbx4>dBd-;{=v66md zkBm0gm_I9qbnN|Y5fV{!7naT>XPBIzt)%?(cqJ>Adqf g~V7p7i516a+Y zxU*nyTbKNwi+jl@$vJe#hWI_{<99DX5U 4zXJZG7A JlKUox_&$Ru?^mKWYA>*~(uOBcH^%74 z++R=E01^!ehBc;a5#ve*KVV~MH@vSW8X)%Yv %4L}y z!MgI^0hsJxAV#HuIO9B$O|0yZ{Zcy>UyOE~0lw|tz%BW;A5g)5Ls4HYEG~_?v~d5# z>ZUUNu4pi(6itaWUr805cCuSEB}~1nCZ+FrL6T#(I6sOA|9P<(YtrySqkd7XehTR` zASJ1NxlIe+vr<&^Fl!j_p*}@qkIYRKaW QU|LN*llMqnmMp0>s{laEH&Pnuh zgU?V6Q)$qWYPj%sK-D$8Sd><6nBV`NQYgUqE=fTw_)zcN8{&ssXvshMMG6FgAOM>u zGJjg4za~%Mn8Kw(VYOk>MZqhd+EFAKz{g4zLCsu4>D>w(he&7ft>lDX!WLrGpaZV? zOIwxbi#7Lg@gXr&gO^M^%foxq)^YBY{4b}y3;mPq=EJw7_iR?PpVa0b&BRs_a@m2Z z!~!E?QAFJ}K^mk;4Tzn^(?U+%fFR08od2!m5SEkK*Z o6s2)rA^lql<~jeBfFZQ;r}iFG321s|Bb4vng{*H`mdjA zEd>z*`!AqFLoLHd Kb=kuRqW zouSPWJ2QO?*H1;aQsAZ8W&5vr{dxVR=WSWy1)H**0$VdD{aN0ZjydP+4s*cQ*H|8q z&JaoHR;o2?`xu@$M?v#T$QjI0LyvHep)~*PATJTfk=?lO_0ATfH2jP2(h1uE9ZVU3 zsQaoKd{p6g-mR}4ht6tDOAf^#bfjz)L-l-9P}yjw`OQ{rdDP~id#VL!D-8zi%B-tR z%zo5^Xz28o2euMy+BZ>YD1PVzV8%=9fN`6v!VAt9);G>pGb$C6{+Mf71$Zv?$*5P_ z0J|)~?0n};PVJs|fN{AerhsC-9LnyOZdtR0wT5o9f0mSD(VosS`ap!^(_#B@Ar7 z=m7y@#X1>3U6Df6U3G*y%T%098Olv@8lOX^D9MlvJJfNF$r9aXO5HV(h% +>iE{+rC|HC^yQWat20cf+DqWzv0&C lY;KUX_yWVqK* z!q;bJ>|8`*IaO8gQG$~Kdin#(dvJhB1#7 m#n8f)H~IvR$#RH3v@cAKbO z)C3`;9qH6Au2@SARLRka?*6;OyvlQGT_39bX@=Uq`SjgdqcBx3LCFK4NQ<@ z>iHOPs_wiPD&=gMykW!S5kfYJR7F^A$d(Zf